🥃

gin-contrib/timeout で context が書き換わる可能性に注意

に公開

この記事は カオナビ Advent Calendar 2025 シリーズ3の9日目です。

Go で Gin を使う時、 gin-contrib/timeout などを使ってタイムアウト制御を入れることがありますよね。
その場合、次のように Context から値を取り出したとき、必ず同じ値が返ってくるとは限らないので注意が必要ですよ、という記事です。これは後述する Gin の Context Pool の仕様によるものです。

id1, _ := c.Get("id")
time.Sleep(50 * time.Millisecond) // 重いSQLなど、何かしら時間がかかる処理
id2, _ := c.Get("id") // id1とid2が異なる可能性がある

Context Pool

gin-contrib/timeout などを利用した場合、 timeout が発生するとクライアントに Status: 408 のレスポンスが返り、利用していた *gin.Context が Pool に戻ります。その一方で基本的に HandlerFunc の処理は実行され続けます。

その後、次のリクエストが来ると、先ほど Pool に入れた Context が Pool から取り出され、 Context が次のリクエスト情報で上書きされます。すると既に実行中だった HandlerFunc の Context は上書きされてしまうため、 c.Get() の値が上書きされた情報を返してしまうようになります。

*gin.Context を Pool に出し入れしているのはソースコードでいうと次の箇所です。

https://github.com/gin-gonic/gin/blob/2a794cd0b0faa7d829291375b27a3467ea972b0d/gin.go#L662-L675

図にすると次のようなイメージです。 Context Pool が同じ Context を返す場合があるため、リクエスト1とリクエスト2が同じ Context を参照してしまっています。

検証コード

次のテストコードで検証しました。

package main

import (
	"context"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"
	"time"

	"github.com/gin-contrib/timeout"
	"github.com/gin-gonic/gin"
)

func TestLargeResponse(t *testing.T) {
	const requestCount = 4 // 2以上の時にエラーになる
	wg := sync.WaitGroup{}
	wg.Add(requestCount)
	counter := 0
	r := gin.New()
	r.Use(
		timeout.New(
			timeout.WithTimeout(40*time.Millisecond),
			timeout.WithResponse(func(c *gin.Context) {
				c.String(http.StatusRequestTimeout, "timeout")
			}),
		),
		func(c *gin.Context) {
			c.Set("id", counter)
			counter++
		},
	)
	r.GET("/", func(c *gin.Context) {
		id1, _ := c.Get("id")

		// 重いDBクエリや外部API呼び出しなど、時間がかかる処理
		time.Sleep(50 * time.Millisecond)

		id2, _ := c.Get("id")
		if id1 != id2 {
			t.Logf("Context overwritten! id1: %v, id2: %v", id1, id2)
			t.Fail()
		}
		c.Status(http.StatusOK)
		wg.Done()
	})
	for range requestCount {
		w := httptest.NewRecorder()
		req, _ := http.NewRequestWithContext(
			context.Background(), "GET", "/", nil)
		r.ServeHTTP(w, req)
	}
	wg.Wait()
}

このテストを実行すると以下のようにテストが失敗します。

% go test 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> gin-timeout-test.TestLargeResponse.func3 (3 handlers)
--- FAIL: TestLargeResponse (0.17s)
    main_test.go:38: Context overwritten! id1: 0, id2: 1
    main_test.go:38: Context overwritten! id1: 1, id2: 2
    main_test.go:38: Context overwritten! id1: 2, id2: 3
FAIL
exit status 1
FAIL    gin-timeout-test        0.806s

おしまい

以上のように Context を使う場合は注意が必要です。例えば GORM を使っている場合、 GORM に Context を渡すと基本的にはこの問題を回避できると思いますので、そうした対応を取ると良さそうです。ほか何か良い回避方法などあればコメント欄で教えていただければと思います。

以上です。

(実は カオナビ Advent Calendar 2025 シリーズ3の4日目とネタがかぶっているのですが、せっかく書いたので公開しちゃいます)

Discussion