Files
zip/detach_test.go
zeekayandHanzo Dev 932d6a5075 fix(caller): a context kept past its request reaches no request
Both transports reuse a RequestCtx for the next request, and a handler's
context read the request's headers lazily, so a context kept past its handler
answered with whichever caller the RequestCtx was serving by then. The context
now holds the request through an inflight the request owns: the server closes
it when it resets the request, and every read holds it while reading. A context
kept past its request answers with no caller, forwards no identity, attests no
peer and is not a local call.


Co-authored-by: Hanzo Dev <[email protected]>
2026-10-03 19:09:23 -07:00

101 lines
2.9 KiB
Go

package zip
import (
"context"
"net/http/httptest"
"sync"
"testing"
"github.com/valyala/fasthttp"
)
// TestDetachOutlivesTheRequest pins that a detached context answers with the
// caller its request carried after that request is finished and its RequestCtx
// is back in the server's pool, and that it no longer reaches the request.
func TestDetachOutlivesTheRequest(t *testing.T) {
app := New(Config{AppName: "svc", DisableStartupMessage: true})
type none struct{}
var (
mu sync.Mutex
det []context.Context
)
app.Get("/v1/hook", func(ctx context.Context, _ *none) (*none, error) {
mu.Lock()
det = append(det, Detach(ctx))
mu.Unlock()
return &none{}, nil
}, WithOperationID("hook"))
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
for _, org := range []string{"acme", "globex", "initech"} {
req := httptest.NewRequest("GET", "/v1/hook", nil)
req.Header.Set(HeaderOrg, org)
req.Header.Set(HeaderUser, "u-"+org)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("%s: %v", org, err)
}
_ = resp.Body.Close()
}
mu.Lock()
defer mu.Unlock()
for i, org := range []string{"acme", "globex", "initech"} {
ctx := det[i]
if behind, _ := request(ctx, func(*fasthttp.RequestCtx) {}); behind {
t.Fatalf("%s: a detached context still reaches its request", org)
}
if err := ctx.Err(); err != nil {
t.Fatalf("%s: a detached context ended with its request: %v", org, err)
}
if c := CallerOf(ctx); c.Org != org || c.User != "u-"+org {
t.Fatalf("%s: detached caller is %+v", org, c)
}
}
}
// TestAContextKeptPastItsRequestReachesNoOne pins the backstop under Detach: a
// handler's own context, kept after its request ended, answers with no caller
// and as no local call — never with the caller of the request the RequestCtx
// serves next.
func TestAContextKeptPastItsRequestReachesNoOne(t *testing.T) {
app := New(Config{AppName: "svc", DisableStartupMessage: true})
type none struct{}
var (
mu sync.Mutex
kept []context.Context
)
app.Get("/v1/keep", func(ctx context.Context, _ *none) (*none, error) {
mu.Lock()
kept = append(kept, ctx)
mu.Unlock()
return &none{}, nil
}, WithOperationID("keep"))
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
for _, org := range []string{"acme", "globex"} {
req := httptest.NewRequest("GET", "/v1/keep", nil)
req.Header.Set(HeaderOrg, org)
req.Header.Set(HeaderUser, "u-"+org)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("%s: %v", org, err)
}
_ = resp.Body.Close()
}
mu.Lock()
defer mu.Unlock()
for i, ctx := range kept {
if c := CallerOf(ctx); c != (Caller{}) {
t.Fatalf("request %d: a context kept past its request answers as %+v", i, c)
}
if Local(ctx) {
t.Fatalf("request %d: a context kept past its request reads as a local call", i)
}
if PeerOf(ctx) != nil || Header(ctx, HeaderOrg) != "" {
t.Fatalf("request %d: a context kept past its request still reads the request", i)
}
}
}