はじめに TIG所属の大江です。Go 1.23リリース連載 の7本目です。
本記事では以下の内容を取り上げます。
netでKeep-Aliveの詳細な設定が可能になりました
net/httpにおけるCookieの扱いのアップデート
1. netでKeep-Aliveの詳細な設定が可能になりました Keep-Aliveに関してより細かい設定が出来るようになりました。
新しく導入されたtype、KeepAliveConfigによって指定できます。
KeepAliveConfigは以下のように定義されています。
type KeepAliveConfig struct { Enable bool Idle time.Duration Interval time.Duration Count int }
次に各パラメーターについてご紹介します。
Enable
Keep-Alive Probeを送るかどうかを決定する。trueの場合送信する
検証したところ、Keep-Alive Probeを一切送らないようにするにはDialのフィールド、KeepAliveも負の数にする必要があります。以下のように設定するとKeep-Alive Probeが送信されなくなりました。
kac := net.KeepAliveConfig{ Enable: false , Idle: 10 * time.Second, Interval: 10 * time.Second, Count: 100 , } transport := &http.Transport{ Dial: (&net.Dialer{ KeepAliveConfig: kac, KeepAlive: -1 * time.Second, }).Dial, }
その他の場合もどの設定が適用されるかを確認しました。以下の表の通りです。
Idle
最初のKeep-Alive Probeを送信するまでにコネクションがアイドル状態である時間
0だとデフォルトの15秒に設定される
デフォルトの15秒はGo 1.22までの値と同じです。
Interval
Keep-Alive Probeが送信される間隔
0だとデフォルトの15秒に設定される
デフォルトの15秒はGo 1.22までの値と同じです。
Count
コネクションを切るまでに送るKeep-Alive Probeの回数
この回数応答がなければコネクションを閉じる。0だとデフォルトの9回に設定される
デフォルトの9回はLinuxのデフォルト設定と同じです。
パケットキャプチャしてみる 以下のサーバーとクライアントを立て、WiresharkでKeep-Alive Probeをキャプチャしてみます。
サーバー側のコード package mainimport ( "fmt" "net/http" ) func main () { http.HandleFunc("/" , func (w http.ResponseWriter, r *http.Request) { fmt.Println("keep-alive start" ) }) s := &http.Server{ Addr: ":8080" , } if err := s.ListenAndServe(); err != nil { panic (err) } }
クライアント側のコード package mainimport ( "io" "log" "net" "net/http" "time" ) func main () { c := &http.Client{ Transport: &http.Transport{ Dial: (&net.Dialer{ KeepAliveConfig: net.KeepAliveConfig{ Enable: true , Idle: 5 * time.Second, Interval: 5 * time.Second, Count: 10 , }, }).Dial, }, } resp, err := c.Get("http://localhost:8080" ) if err != nil { panic (err) } defer resp.Body.Close() if _, err := io.Copy(io.Discard, resp.Body); err != nil { panic (err) } time.Sleep(100 * time.Second) }
5秒ごとにKeep-Alive Probeが送られていることが確認できました。
2. net/httpにおけるCookieの扱いのアップデート 今回のアップデートでは、Cookieに関するアップデートがいくつか入りました。
まずはCookie関連のアップデートのポイントを紹介します。
新規追加されたCookie.Quotedフィールドにより、Cookieの値が元々クォートされていたかどうかを判定できるようになりました
新規追加されたRequest.CookiesNamed()により、指定された名前と一致するすべてのCookieを取得できるようになりました
新規追加されたParseSetCookie()、ParseCookie()により、Set-CookieヘッダーからCookieを取得できるようになりました。ParseCookie()では、同じCookie名が複数回現れてもすべてのCookieを取得できます
新規追加されたCookie.Partitionedフィールドにより、Partitioned属性を持つCookieを識別できるようになりました
新規追加されたCookie.Quotedフィールドにより、Cookieの値が元々クォートされていたかどうかを判定できるように まず一点目のアップデートについてご紹介します。
修正の元となったIssueがこちら です。
Cookieについての仕様は、RFC6265の一部 として以下のように策定されています。
cookie-pair = cookie-name "=" cookie-value ... cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
またクオーテーションについての扱いについて、RFC6265のドラフトのコメント では以下のように書かれています。
Per the grammar above, the cookie-value MAY be wrapped in DQUOTE characters. Note that in this case, the initial and trailing DQUOTE characters are not stripped. They are part of the Cookie-value, and will be included in Cookie headers sent to the server.
RFCにはっきり明記されている訳ではないので人によって解釈が分かれるところではありましたが、最終的にはRFCはクオーテーションをCookie-valueの一部としているという解釈に落ち着いたようです。
現状のnet/httpパッケージはCookie-valueをダブルクォーテーションを含まないものとして扱っているので、ダブルクォーテーションを残しておく修正が入りました。
結論として以下のような仕様に落ち着きました。
http.Cookieに新たなフィールドQuoted boolを追加する
Cookieをparseするときに、値からダブルクォートが削除された場合、Quoted=trueに設定する
Cookieを出力する際、もしQuoted=trueの場合は、ダブルクォートを値に再度追加する
スペースやカンマを含むCookieについては、Go言語の古いバージョンとの互換性のために、ダブルクォートが暗黙的に追加される
新旧比較 それでは実際に動かしてどのような点が変わったのか試してみます。
以下のコードをGo 1.23と1.22で動かしてみます。
処理内容としては以下です。
Set-Cookieヘッダーを返すテストhttpサーバーを立てる
テストHTTPサーバーに対してGET要求を送る。サーバーが返したSet-Cookieヘッダーに基づきCookieがクライアントのcookiejarに保存される
2回目のGet要求でCookieをサーバーに送信する
ここでは以下二点を確認します。
2のSet-CookieヘッダーからcookiejarにセットしたCookieインスタンスの中身
3でサーバーに送信されたCookieヘッダーの中身
実行したコードは以下です。
package mainimport ( "fmt" "io" "log" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" ) func main () { ts := httptest.NewServer(http.HandlerFunc(func (w http.ResponseWriter, resp *http.Request) { w.Header().Add("Set-Cookie" , `blank=` ) w.Header().Add("Set-Cookie" , `no_quotation=value` ) w.Header().Add("Set-Cookie" , `only_double_quotation=""` ) w.Header().Add("Set-Cookie" , `with_double_quotation="value"` ) w.Header().Add("Set-Cookie" , `with_space=va lue` ) w.Header().Add("Set-Cookie" , `with_comma=va,lue` ) fmt.Printf("Cookie: %v" , resp.Header["Cookie" ]) })) defer ts.Close() jar, err := cookiejar.New(nil ) if err != nil { panic (err) } client := &http.Client{Jar: jar} resp1, err := client.Get(ts.URL) if err != nil { panic (err) } defer resp1.Body.Close() if _, err := io.Copy(io.Discard, resp1.Body); err != nil { panic (err) } parsedURL, err := url.Parse(ts.URL) if err != nil { panic (err) } for i, cookie := range jar.Cookies(parsedURL) { fmt.Printf("%v.\nName: %v\nValue: %v\nQuoted: %v\n" , i+1 , cookie.Name, cookie.Value, cookie.Quoted) } resp2, err := client.Get(ts.URL) if err != nil { panic (err) } defer resp2.Body.Close() if _, err := io.Copy(io.Discard, resp2.Body); err != nil { panic (err) } }
まずSet-CookieヘッダーcookiejarにどのようなCookieが入るかですが、以下のようになりました。
Go 1.23での実行結果 1. Name: blank Value: Quoted: false 2. Name: no_quotation Value: value Quoted: false 3. Name: only_double_quotation Value: Quoted: true 4. Name: with_double_quotation Value: value Quoted: true 5. Name: with_space Value: va lue Quoted: false 6. Name: with_comma Value: va,lue Quoted: false
Go 1.22以前の実行結果(Quotedの部分を削除して実行) 1. Name: blank Value: 2. Name: no_quotation Value: value 3. Name: only_double_quotation Value: 4. Name: with_double_quotation Value: value 5. Name: with_space Value: va lue 6. Name: with_comma Value: va,lue
Go 1.22以前と以降でCookie.Valueの値は変わらず、ダブルクォーテーションが元々ついていた場合はCookie.Quoted=trueになっています。
Cookie.Valueの中身は変えないことで、Go 1.22以前の動作に影響を与えずかつクオーテーションの情報を付与するということが実現できているようです。
次に、サーバーに送信されたCookieヘッダーの中身を比較します。
Go 1.23での実行結果 Cookie: [blank=; no_quotation=value; only_double_quotation=; with_double_quotation="value" ; with_space="va lue" ; with_comma="va,lue" ]
Go 1.22以前の実行結果(Quotedの部分を削除して実行) Cookie: [blank=; no_quotation=value; only_double_quotation=; with_double_quotation=value; with_space="va lue" ; with_comma="va,lue" ]
cookie.Quoted=trueの場合は、ダブルクォーテーションがCookieヘッダーの前後に追加されて送信されます。 ちなみに、Cookieの値がダブルクォーテーションのみの場合は、ダブルクォーテーションだけになるのではなく空になりました。
新規追加されたRequest.CookiesNamed()により、指定された名前と一致するすべてのCookieを取得できるように 同じNameのCookieがブラウザから送られてきたとき、Request.Cookie()では指定した名前に一致する最初のCookieしか取得できませんでした。
Request.CookiesNamed()によって、同じNameの全てのCookieを取得できるようになりました。
package mainimport ( "fmt" "io" "log" "net/http" "net/http/httptest" ) func main () { ts := httptest.NewServer(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { fmt.Println(r.Cookie("duplicate_name" )) fmt.Println(r.CookiesNamed("duplicate_name" )) })) defer ts.Close() req, err := http.NewRequest(http.MethodGet, ts.URL, nil ) if err != nil { panic (err) } cookies := []*http.Cookie{ {Name: "duplicate_name" , Value: "value" }, {Name: "duplicate_name" , Value: "value2" }, {Name: "different_name" , Value: "value3" }, } for _, cookie := range cookies { req.AddCookie(cookie) } resp, err := http.DefaultClient.Do(req) if err != nil { panic (err) } defer resp.Body.Close() if _, err := io.Copy(io.Discard, resp.Body); err != nil { panic (err) } }
実行結果 duplicate_name=value <nil > [duplicate_name=value duplicate_name=value2]
CookiesNamed()では同じ名前(“duplicate_name”)の2つの値(“value”,”value2”)を両方取り出すことが出来ました。
新規追加されたParseSetCookie()、ParseCookie()により、Set-CookieヘッダーからCookieを取得できるように 両者ともSet-CookieヘッダーからCookie型を取得するメソッドです。
ParseSetCookieは1つ、ParseCookieは複数のCookieを取得します。
package mainimport ( "fmt" "io" "net/http" "net/http/httptest" ) func main () { ts := httptest.NewServer(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { w.Header().Add("Set-Cookie" , `cookie1="value1"` ) w.Header().Add("Set-Cookie" , `cookie2=value2` ) })) defer ts.Close() resp, err := http.Get(ts.URL) if err != nil { panic (err) } defer resp.Body.Close() if _, err := io.Copy(io.Discard, resp.Body); err != nil { panic (err) } for _, setCookie := range resp.Header["Set-Cookie" ] { cookie, err := http.ParseSetCookie(setCookie) if err != nil { panic (err) } fmt.Println(cookie) } var cookiesString string for _, setCookie := range resp.Header["Set-Cookie" ] { cookiesString += setCookie + ";" } cookiesString = cookiesString[:len (cookiesString)-1 ] cookies, err := http.ParseCookie(cookiesString) if err != nil { panic (err) } fmt.Println(cookies) }
実行結果 cookie1="value1" cookie2=value2 [cookie1="value1" cookie2=value2]
新規追加されたCookie.Partitionedフィールドにより、Partitioned属性を持つCookieを識別できるように CHIPS に対応するため、Cookie.Partitionedフィールドが追加されました。
CHIPSとは、サードパーティーCookieを安全に扱えるようにするためGoogleが策定している仕様です。CHIPSでは、Partitioned属性が入っている場合、特定のトップレベルドメインのみに有効なサードパーティーCookieとして保存されます。
Chromeは2025年初頭から、現行のサードパーティCookieの廃止に向けた取り組みを進める予定です。この対応が行われると、CHIPSに対応している場合のみにサードパーティーCookieを使えるようになります。 そちらへ対応するため、今回Go 1.23へのアップデート項目に入りました。
以下が策定された仕様です。
Cookie parserで、Cookieに”; Partitioned”が入っている場合、boolはtrueに設定されます
Cookie.Stringでは、Partitionedがtrueの場合、文字列に”; Partitioned”が加えられます
Cookie.Validでは、Partitionedがtrueで、Cookieがセキュアでない場合、Validはエラーを返します
以下実行したコードです。
package mainimport ( "fmt" "log" "net/http" ) func main () { cookieParsed, err := http.ParseSetCookie("parsed_cookie=parsed_cookie; SameSite=None; Secure; HttpOnly; Path=/; Partitioned" ) if err != nil { panic (err) } cookies := []*http.Cookie{ cookieParsed, {Name: "secure_partitioned" , Value: "secure_partitioned" , Path: "/" , Secure: true , Partitioned: true }, {Name: "no_secure_partitioned" , Value: "no_secure_partitioned" , Path: "/" , Partitioned: true }, {Name: "no_partitioned" , Value: "no_partitioned" , Path: "/" , Secure: true }, } for _, cookie := range cookies { fmt.Printf("{\"String\":\"%v\", \"Valid\":\"%v\", \"Partitioned\":\"%v\"}\n" , cookie.String(), cookie.Valid(), cookie.Partitioned) } }
実行結果 ※cookie.Valid() はValidだとnil,Validでないとエラーメッセージが出力されます。
{"String" :"parsed_cookie=parsed_cookie; Path=/; HttpOnly; Secure; SameSite=None; Partitioned" , "Valid" :"<nil>" , "Partitioned" :"true" } {"String" :"secure_partitioned=secure_partitioned; Path=/; Secure; Partitioned" , "Valid" :"<nil>" , "Partitioned" :"true" } {"String" :"no_secure_partitioned=no_secure_partitioned; Path=/; Partitioned" , "Valid" :"http: partitioned cookies must be set with Secure" , "Partitioned" :"true" } {"String" :"no_partitioned=no_partitioned; Path=/; Secure" , "Valid" :"<nil>" , "Partitioned" :"false" }
おわりに net,net/httpパッケージの一部アップデート項目についてのご紹介でした。
個人的には、初めて言語仕様変更の議論を追ってみて、当初の提案から議論を経て修正されていく様子を見るのが興味深かったです。
また今回ご紹介したものには数年前に挙がったIssueが発端となり修正されたものが多く、長い綿密な議論を経てアップデートされていることも印象的でした。
次は棚井さんのGo Telemetry です。