WebサーバーとブラウザのTCP接続が切れると"context canceled"になる
GoでHTTP/1.1のWebサーバーを実装すると、リクエスト側によるタイムアウト制御などによりブラウザとのTCP接続が切断された際に http.Request.Context()
が "context canceled"
になるようで、この仕様を知らずに少しハマったのでこの記事を書きます。
TCP接続が切断されるケースについて具体例を挙げると、次のような場合が考えられます。
- ブラウザの場合
- タイムアウト制御などによりfetchがcancelされた時
- ページ遷移によりfetchがcancelされた時
- curlの場合
-
-m
オプションでタイムアウト設定を行いタイムアウトした時 - curlをkillした時
-
なお、TCP接続を切断した時に "context canceled"
にしているのは net/http/server.go で実装されています。
GETメソッドの実装例
例えば次のWebサーバーの実装があったとします(ginとnet/httpどちらでも同じ挙動になるので、それぞれの例を記載しておきます)。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.ContextWithFallback = true
r.GET("/", func(c *gin.Context) {
<-c.Done()
fmt.Println(c.Err().Error()) // "context canceled"
})
r.Run(":8080")
}
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
fmt.Println(r.Context().Err().Error()) // "context canceled"
})
http.ListenAndServe(":8080", nil)
}
上記コードで立ち上げたWebサーバーに対して、ブラウザから次のコードでリクエストすると、Webサーバーでは3秒後に "context canceled"
が出力されます。
// 3秒でタイムアウト
let controller = new AbortController();
setTimeout(controller.abort.bind(controller), 1000 * 3);
fetch('http://localhost:8080/', {
method: 'GET',
signal: controller.signal
});
curlの場合は次のコマンドを叩くことで、同じように3秒後に "context canceled"
が発生します。 (-m 3
で3秒のタイムアウトを設定しています)
curl --http1.1 -m 3 -X GET http://localhost:8080/
POSTやPATCHなどの実装例
POSTやPATCHメソッドなどの場合、リクエストBodyを送信する事になりますが、その場合は少し挙動が変わるようでした。ginを使った場合はBodyを読み込んだ時にだけ "context canceled"
が発生するようです。Bodyを読み込まないと "context canceled"
は発生しないようなので注意が必要です。
package main
import (
"fmt"
"io"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.ContextWithFallback = true
r.POST("/", func(c *gin.Context) {
// Bodyを読み込む
_, _ = io.ReadAll(c.Request.Body)
// または c.ShouldBindJSON(&struct{}{}) など
<-c.Done()
fmt.Println(c.Err().Error()) // "context canceled"
})
r.Run(":8080")
}
上記コードで立ち上げたWebサーバーに対して、ブラウザから次のコードでリクエストすると、Webサーバーでは3秒後に "context canceled"
が出力されます。
GETメソッドの時との違いはbodyを含んでいる事です。bodyを含まない場合は "context canceled"
が発生しないようです。
// 3秒でタイムアウト
let controller = new AbortController();
setTimeout(controller.abort.bind(controller), 1000 * 3);
fetch('http://localhost:8080/', {
method: 'POST',
body: {},
signal: controller.signal
});
curlの場合は次のようにbodyを含めることで3秒後に "context canceled"
が発生します。
curl --http1.1 -m 3 \
-X POST \
-H "Content-Type: application/json" -d '{}' \
http://localhost:8080/
終わり
検証に使ったバージョンは以下の通りです。
- go 1.21.0
- gin v1.9.1
以上です
Discussion