🕸️

WebサーバーとブラウザのTCP接続が切れると"context canceled"になる

2023/11/05に公開

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どちらでも同じ挙動になるので、それぞれの例を記載しておきます)。

ginの実装例
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")
}
net/httpの実装例
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" が出力されます。

jsからリクエストを送信する場合
// 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からリクエストを送信する場合
curl --http1.1 -m 3 -X GET http://localhost:8080/

POSTやPATCHなどの実装例

POSTやPATCHメソッドなどの場合、リクエストBodyを送信する事になりますが、その場合は少し挙動が変わるようでした。ginを使った場合はBodyを読み込んだ時にだけ "context canceled" が発生するようです。Bodyを読み込まないと "context canceled" は発生しないようなので注意が必要です。

ginの場合
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" が発生しないようです。

jsからリクエストを送信する場合
// 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からリクエストを送信する場合
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