8. レスポンスの分解 — status-line、field-line、そして本文の長さをどう決めるか
レスポンスもテキストで、1 行目が (HTTP-version status-code SP reason-phrase)、の行、空行、です。分解の難所は 1 つだけで、「本文はどこで終わるか」です。
9112 §6.3 は本文の長さの決め方を順番つきで定めています。この実装が使うのは 3 つ。 があれば chunk を解いて長さを知る( より優先)。Content-Length があればそのバイト数。どちらも無ければ接続が閉じるまでが本文。前のレッスンで を付けたので、3 つ目の「閉じるまで」が常に使えますが、本物のサーバーは chunked で返してくることが多いので(example.com がそうです)、chunk の復号も入れてあります。
行末は が正式ですが、RFC 9112 §2.2 は「受信側は 単独を行末として認識してよい(先行する CR は無視してよい)」としています。参照実装は、ヘッダー部と本文の境目を CRLF CRLF と LF LF の早い方で見つけ、ヘッダーの行は で読みます(lines() は \r\n も \n も行末として扱います)。本文は手を加えずに切り出します。chunk の長さはバイト数なので、本文の中の CRLF を勝手に LF に直すと長さが狂います。
ヘッダー名は大文字小文字を区別しません(RFC 9110 §5.1)。だから header("content-type") でも Content-Type が引けるように、eq_ignore_ascii_case で比べます。status-code は 3 桁の数字で、それ以外なら InvalidStatusLine、":" の無いヘッダー行なら InvalidHeader、gzip のような知らない なら UnsupportedBody を返します。
本物のブラウザでは: ではヘッダーの分解を HttpStreamParser が行い、結果は HttpResponseHeaders に入ります。chunked の復元は http/http_chunked_decoder.cc で、ファイル先頭に「Derived from: mozilla/netwerk/protocol/http/src/nsHttpChunkedDecoder.h」とあり、Firefox の nsHttpChunkedDecoder.cpp と同じ祖先です。この回の decode_chunked() は、その 2 つと同じ RFC 9112 の を読んで書いたものです。本文はからへ のデータパイプで 64KB ずつ渡されます。1 回の () で全部読むこの実装との差は、届いた分から順にへ流せるかどうかです。
開発のどこに活かすか: 「本文の長さは 3 通りの決め方があり、優先順位がある」ことを知らないと、自作のクライアントやプロキシが chunked の応答で固まります。逆に、API サーバーを書く側では Content-Length を正しく出すか chunked にするかで、クライアントの読み終わり方が変わります。ヘッダー名の大文字小文字を区別しない比較も、自前でヘッダーを触るコードで毎回出てくる約束です。
点線の付いた語はタップすると説明が出ます。用語集を開く →
サンプルコード(フレームワーク環境が必要なため表示のみ)
// src/http.rs の分解の部分(HttpResponse::parse と decode_chunked)
pub fn parse(raw: &str) -> Result<HttpResponse, HttpError> {
if raw.is_empty() {
return Err(HttpError::EmptyResponse);
}
// ヘッダー部と本文の境目 = 最初の空行(CRLF CRLF、または LF LF)
let (head, body) = split_head_body(raw);
let mut lines = head.lines(); // lines() は "\r\n" も "\n" も行末として扱う
let status_line = lines.next().unwrap_or("");
// status-line = HTTP-version SP status-code SP [ reason-phrase ]
let mut parts = status_line.splitn(3, ' ');
let version = parts.next().unwrap_or("");
let code = parts.next().unwrap_or("");
let reason_phrase = parts.next().unwrap_or("").trim_end().to_string();
if !version.starts_with("HTTP/") || code.len() != 3 {
return Err(HttpError::InvalidStatusLine(status_line.to_string()));
}
let status_code = code
.parse::<u16>()
.map_err(|_| HttpError::InvalidStatusLine(status_line.to_string()))?;
// field-line = field-name ":" OWS field-value OWS
let mut headers = Vec::new();
for line in lines {
if line.trim().is_empty() {
continue;
}
let (name, value) = line
.split_once(':')
.ok_or_else(|| HttpError::InvalidHeader(line.to_string()))?;
headers.push(Header {
name: name.trim().to_string(),
value: value.trim().to_string(),
});
}
let mut response = HttpResponse {
version: version.to_string(),
status_code,
reason_phrase,
headers,
body: String::new(),
};
// 本文の長さの決め方(RFC 9112 §6.3 の順番): Transfer-Encoding が Content-Length より優先
response.body = match response.header("Transfer-Encoding") {
Some(te) if te.eq_ignore_ascii_case("chunked") => decode_chunked(body)?,
Some(te) => return Err(HttpError::UnsupportedBody(format!("Transfer-Encoding: {te}"))),
None => match response.header("Content-Length").and_then(|v| v.trim().parse::<usize>().ok()) {
// Content-Length があればそのバイト数まで(文字の途中で切れるなら全部)
Some(len) => body.get(..len).unwrap_or(body).to_string(),
// どちらも無ければ「接続が閉じるまで」= 受け取った全部
None => body.to_string(),
},
};
Ok(response)
}
fn decode_chunked(body: &str) -> Result<String, HttpError> {
let mut out = String::new();
let mut rest = body;
loop {
let (size_line, after) = rest
.split_once('\n')
.ok_or_else(|| HttpError::UnsupportedBody("chunk-size の行がありません".into()))?;
let size_hex = size_line.split(';').next().unwrap_or("").trim();
let size = usize::from_str_radix(size_hex, 16)
.map_err(|_| HttpError::UnsupportedBody(format!("chunk-size が 16 進でない: {size_hex:?}")))?;
if size == 0 {
break; // last-chunk。trailer は読まない
}
let data = after
.get(..size)
.ok_or_else(|| HttpError::UnsupportedBody("chunk-data が足りません".into()))?;
out.push_str(data);
// chunk-data の後ろの CRLF(または LF)を飛ばす
rest = &after[size..];
rest = rest.strip_prefix("\r\n").or_else(|| rest.strip_prefix('\n')).unwrap_or(rest);
}
Ok(out)
}
手元で動かす(配布物)
- ステップ 2 の 1 ファイル版(step02_http_message.rs)分解のテスト(CRLF / LF / Content-Length / chunked / 壊れた入力)が入っている
図やコードに出てくる用語(タップで説明)
学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。
公式ドキュメントで詳しく ↗