はじめに
TIGの辻です。Go 1.19連載の4本目です。
Go Release Notes に記載がある Minor changes to the library の net/http
の3点のアップデートについて触れていきます。1
ResponseWriter.WriteHeader
がユーザーが定義した 1xx 系の情報レスポンスをサポートしたMaxBytesReader
が読み取り制限を超過した場合にMaxBytesError
を返却するようになったLocation
ヘッダーのない 3xx 系のレスポンスをエラーとして扱わないようになった
(1)ResponseWriter.WriteHeader
がユーザーが定義した 1xx 系の情報レスポンスをサポートした
Go 1.19 からユーザーが定義したHTTPのレスポンスコード 1xx 系の情報ヘッダーをサポートするようになりました。Go 1.18 まではGoの net/http
を使ったHTTPサーバでステータスコード 1xx 系を書き込むことはできませんでした。関連するIssue は以下などが挙げられます。
- net/http: Server/Handler/ResponseWriter doesn’t allowing sending arbitrary 1xx responses
- net/http: support status code 102 (Processing) in ResponseWriter
- net/http/httputil: make ReverseProxy forward 1xx responses
Goの改善内容の詳細を紹介する前に 1xx のステータスコードがどのようなものであるか、どのような挙動になるか簡単におさらいしておきます。
1xx のステータスコード
1xx は情報レスポンスと呼ばれています。
The 1xx (Informational) class of status code indicates an interim response for communicating connection status or request progress prior to completing the requested action and sending a final response. 1xx responses are terminated by the first empty line after the status-line (the empty line signaling the end of the header section). Since HTTP/1.0 did not define any 1xx status codes, a server MUST NOT send a 1xx response to an HTTP/1.0 client.
Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content より
ポイントは以下の3つです。
- 要求されたアクションを完了し最終的なレスポンスを送信する前に、コネクションの状態やレスポンスの進捗状況を伝えるための中間的なレスポンスである
- 1xx レスポンスは、ステータス行の後の最初の空行(ヘッダーセクションの終わりを示す空行)で終了する
- HTTP/1.0 では定義されていないため、サーバーは HTTP/1.0 クライアントに 1xx レスポンスを送信してはいけない
この挙動は具体的にどのようになるのか 1xx 系のステータスコードの1つである 103 Early Hints を使って確認しておきます。
103 Early Hints
103 Early Hints はクライアントが最終的なレスポンスを処理するために役立つヒントを伝えるために使用できるHTTP ステータスコードです。ヘッダだけを送る目的で使われます。リソース配信の最適化に役に立つ、と考えられています。RFCのステータスは EXPERIMENTAL であり、実験的な仕様です。2
103 Early Hints のレスポンスをクライアントが解釈する場合/しない場合のそれぞれの挙動を補足します。
- 103 Early Hints を解釈する場合の挙動
イメージ図は日本経済新聞社さんのブログ記事「ChromeとFastlyのEarly Hintsの効果計測に貢献する」より引用しています。
図左のクライアントが図右のサーバーに対して GET /index.html
のリソースをリクエストしたときに、そのレスポンスが返却される前にサーバーから 103 Early Hints と link
ヘッダーがレスポンスされます。 103 Early Hints のレスポンスを受け取ったクライアントは GET /index.html
のレスポンスを待たずして link
ヘッダーに記載があるリソース /main.css
にリクエストできます。GET /main.css
とリクエストすることで、最終的な /index.html
で必要なリソース /main.css
を予め取得できます。
- 103 Early Hints を解釈しない場合の挙動
103 Early Hints を解釈しないクライアントの場合、以下のような挙動になります。クライアントは GET /index.html
のレスポンスが返却された後に GET /main.css
とリクエストして最終的なレスポンスに必要なリソースを取得します。
net/http
の挙動の変化
前置きが少し長くなりました。先程紹介した 103 Early Hints と 200 OKを返却するようなHTTPサーバを net/http
を使って実装する場合、以下のようなコードが一例として考えられます。
package main |
Go 1.18までは WriteHeader()
は任意の 1xx 系のステータスコードをサポートしていませんでした。このことは WriteHeader のドキュメントにも記載があります。
Go does not currently support sending user-defined 1xx informational headers, with the exception of 100-continue response header that the Server sends automatically when the Request.Body is read.
そのため、上記の実装を Go 1.18 でビルド&起動したサーバに、クライアントからHTTPリクエストしても機能しません。103 Early Hints のレスポンスはクライアントとのコネクションに書き込まれますが、200 OK のレスポンスはサーバーから書き込まれません。curl
3 のクライアントではサーバーからのレスポンスを待ち続けます。
> curl -LIXGET localhost:8080 |
一方、Go 1.19 でビルド&起動すると、想定どおり 103 Early Hints と 200 OK の両方のレスポンスが得られます。想定どおり機能していることがわかります。なお Link
ヘッダーは最終的なレスポンスにも含まれることに注意してください。
> curl -LIXGET localhost:8080 |
Goのパッチ内容
パッチは 269997: net/http: allow sending 1xx responses で進められていました。WriteHeader()
でステータスコードを書き込むときにステータスコードが 1xx のときはHTTPヘッダーと改行をバッファに書き込んで、それをフラッシュする、ということが主です。
func (w *response) WriteHeader(code int) { |
(2)MaxBytesReader
が読み取り制限を超過した場合に MaxBytesError
を返却するようになった
Go 1.18 で追加になったMaxBytesReader でエラーが発生した場合のエラーハンドリングが行いやすくなりました。Go1.18 ではリクエストが大きすぎる場合に errors.New("http: request body too large")
としてAPIのクライアントにエラーを返却していました。このエラー固有のエラーハンドリングを行いたい場合、以下のように文字列で比較してエラーハンドリングする必要がありました。
b, err := io.ReadAll(r.Body) |
Go 1.19ではユーザーがエラーハンドリングしやすいように新たに MaxBytesError
型という error
インタフェースを満たした型を返却するようになりました。元のIssueは net/http: add MaxBytesError #30715 です。
- パッチ内容の一部
+// MaxBytesError is returned by MaxBytesReader when its read limit is exceeded. |
私が興味深く思ったことは、わざわざ新しく maxBytesReader
型でバイトの初期サイズを非公開フィールド i
として保持するようにしているが、エラーメッセージを変更していない点です。Error()
メソッドのコメントによると「Hyrumの法則」に基づくためとのことです。errors.New()
で返却する文字列はGo Docとして公開しているわけではないが、Go 1.18で観測可能なエラー発生時の文字列によりエラーハンドリングを行っているユーザーへの配慮を感じました。なお、Hyrumの法則は『Googleのソフトウェアエンジニアリング』の1章にて以下のように紹介されています。
- Hyrumの法則
あるAPIに十分な数のユーザーがいるとき、APIを作った者自身が契約仕様として何かを約束しているかは重要ではない。作られたシステムが持つあらゆる観察可能(observable)な挙動に関して、それに依存するユーザーが出てくるものである。
(3)Location
ヘッダーのない 3xx 系のレスポンスをエラーとして扱わないようになった
Go1.18 までは 3xx 系のレスポンスコードで Location
ヘッダーがない場合はエラーとして扱っていました。
一方 RFC7231 Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content ではステータスコード 301 における Location
ヘッダーは SHOULD
の項目になります。RFC2119 Key words for use in RFCs to Indicate Requirement Levels にあるように MUST
であれば、絶対的に含めるべき項目になりますが、SHOULD
は推奨される項目です。RFC上は Location
ヘッダーが含まれないことも許容されます。
このことから Go の実装としても Location
ヘッダーがなくてもエラーとせずにレスポンスをAPIの呼び出し元に返却するように改善しました。
リアルワールドでは net/http: can’t read 301 response without a Location header #49281 という課題がありました。AWSが提供するS3のURLにHTTPリクエストしたときにレスポンスコード 301 で返却されたが Location
ヘッダーが含まれないためにGoのエラーが発生する。レスポンスヘッダー x-amz-bucket-region
から想定するリージョンを取得できず、困っていた、とのことです。この挙動は Go1.19 で改善されます。
package main |
まとめ
net/http
のアップデートはリリースノートではさらっと3行記載があるだけですが、それぞれの背景やパッチ内容を含めて紹介しました。リアルワールド感あふれる課題やニーズを感じることでき、とてもわくわくしました。
本記事では net/http
のアップデートを紹介しました。その他にも net/url
で JoinPath と URL.JoinPath が追加になっています。HTTPはもちろんですが、その他のプロトコルにおいても、便利にURLを組み立てられるようになっています。
最後まで読んでいただき、ありがとうございました!
- 1.なお2022年8月2日にGo1.19 rc2で調べています。 ↩
- 2.Chrome と Fastly による実装実験が始まっています。https://www.fastly.com/jp/blog/beyond-server-push-experimenting-with-the-103-early-hints-status-code ↩
- 3.curlのバージョンは7.83.1を使っています。 ↩