Repository: google/go-safeweb (go)

Issue:
SetContext violates the context.Context contract
In all APIs that rely on context there is no way for a callee to change the context of the caller.

Our IncomingRequest.SetContext method allows to do that. We should instead mimic the stdlib way and clone the request.

Alternatively we could explicitly pass context.Context as first parameter, but it would be quite a bit leap from the stlib api.

Pull request:
Interceptors can now carry state throughout the request processing.
Introduces a `safehttp.FlightValues()` function that lets interceptors and handlers refer to a shared state through the `safehttp.IncomingRequest.Context()`.

See `safehttp/flightvalues_test.go` for usage.

Fixes #314.

Patch:
diff --git a/examples/sample-application/secure/auth/auth.go b/examples/sample-application/secure/auth/auth.go
index 1c7efef..b35611a 100644
--- a/examples/sample-application/secure/auth/auth.go
+++ b/examples/sample-application/secure/auth/auth.go
@@ -47,7 +47,7 @@ func (ip Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequ
 	// Identify the user.
 	user := ip.userFromCookie(r)
 	if user != "" {
-		r.SetContext(ctxWithUser(r.Context(), user))
+		putUser(r.Context(), user)
 	}
 
 	if _, ok := cfg.(Skip); ok {
@@ -112,7 +112,7 @@ func (ip Interceptor) userFromCookie(r *safehttp.IncomingRequest) string {
 // Implementation details: to interact with the interceptor, passes data through
 // the IncomingRequest's context.
 func ClearSession(r *safehttp.IncomingRequest) {
-	r.SetContext(ctxWithSessionAction(r.Context(), clearSess))
+	putSessionAction(r.Context(), clearSess)
 }
 
 // CreateSession creates a session.
@@ -120,8 +120,8 @@ func ClearSession(r *safehttp.IncomingRequest) {
 // Implementation details: to interact with the interceptor, passes data through
 // the IncomingRequest's context.
 func CreateSession(r *safehttp.IncomingRequest, user string) {
-	r.SetContext(ctxWithSessionAction(r.Context(), setSess))
-	r.SetContext(ctxWithUser(r.Context(), user))
+	putSessionAction(r.Context(), setSess)
+	putUser(r.Context(), user)
 }
 
 // Skip allows to mark an endpoint to skip auth checks.
diff --git a/examples/sample-application/secure/auth/ctx.go b/examples/sample-application/secure/auth/ctx.go
index 60a4575..31312e2 100644
--- a/examples/sample-application/secure/auth/ctx.go
+++ b/examples/sample-application/secure/auth/ctx.go
@@ -16,13 +16,17 @@
 
 package auth
 
-import "context"
+import (
+	"context"
 
-type ctxKey string
+	"github.com/google/go-safeweb/safehttp"
+)
+
+type key string
 
 const (
-	userCtx       ctxKey = "user"
-	changeSessCtx ctxKey = "change"
+	userKey       key = "user"
+	changeSessKey key = "change"
 )
 
 type sessionAction string
@@ -32,12 +36,12 @@ const (
 	setSess   sessionAction = "set"
 )
 
-func ctxWithSessionAction(ctx context.Context, action sessionAction) context.Context {
-	return context.WithValue(ctx, changeSessCtx, action)
+func putSessionAction(ctx context.Context, action sessionAction) {
+	safehttp.FlightValues(ctx).Put(changeSessKey, action)
 }
 
 func ctxSessionAction(ctx context.Context) sessionAction {
-	v := ctx.Value(changeSessCtx)
+	v := safehttp.FlightValues(ctx).Get(changeSessKey)
 	action, ok := v.(sessionAction)
 	if !ok {
 		return ""
@@ -45,12 +49,12 @@ func ctxSessionAction(ctx context.Context) sessionAction {
 	return action
 }
 
-func ctxWithUser(ctx context.Context, user string) context.Context {
-	return context.WithValue(ctx, userCtx, user)
+func putUser(ctx context.Context, user string) {
+	safehttp.FlightValues(ctx).Put(userKey, user)
 }
 
 func ctxUser(ctx context.Context) string {
-	v := ctx.Value(userCtx)
+	v := safehttp.FlightValues(ctx).Get(userKey)
 	user, ok := v.(string)
 	if !ok {
 		return ""
diff --git a/go.sum b/go.sum
index bf251eb..955f7ac 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,7 @@ github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM=
 github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
+github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -16,6 +17,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
 golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -25,6 +27,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/safehttp/flight.go b/safehttp/flight.go
index 05300ad..c7e58c3 100644
--- a/safehttp/flight.go
+++ b/safehttp/flight.go
@@ -15,6 +15,7 @@
 package safehttp
 
 import (
+	"context"
 	"net/http"
 )
 
@@ -142,3 +143,37 @@ type Result struct{}
 func NotWritten() Result {
 	return Result{}
 }
+
+type flightValues struct {
+	m map[interface{}]interface{}
+}
+
+func (fv flightValues) Put(key, value interface{}) {
+	fv.m[key] = value
+}
+
+func (fv flightValues) Get(key interface{}) interface{} {
+	return fv.m[key]
+}
+
+// Map is a key/value map.
+type Map interface {
+	// Put inserts a key/value pair into the map. If the key already exists in
+	// the map, it's value is replaced.
+	Put(key, value interface{})
+	// Get returns a value for a given key in the map. If the entry with a given
+	// key does not exist, nil is returned.
+	Get(key interface{}) interface{}
+}
+
+type flightValuesCtxKey struct{}
+
+// FlightValues returns a map associated with the given request processing flight.
+// Use it if your interceptors need state that has the lifetime of the request.
+func FlightValues(ctx context.Context) Map {
+	v := ctx.Value(flightValuesCtxKey{})
+	if v == nil {
+		return nil
+	}
+	return v.(Map)
+}
diff --git a/safehttp/incoming_request.go b/safehttp/incoming_request.go
index af0ab11..1a306af 100644
--- a/safehttp/incoming_request.go
+++ b/safehttp/incoming_request.go
@@ -35,10 +35,13 @@ type IncomingRequest struct {
 	TLS *tls.ConnectionState
 	// URL specifies the URL that is parsed from the Request-Line. For most requests,
 	// only URL.Path() will return a non-empty result. (See RFC 7230, Section 5.3)
-	URL                *URL
-	req                *http.Request
-	postParseOnce      sync.Once
-	multipartParseOnce sync.Once
+	URL *URL
+	req *http.Request
+
+	// The fields below are kept as pointers to allow cloning through
+	// IncomingRequest.WithContext. Otherwise, we'd need to copy locks.
+	postParseOnce      *sync.Once
+	multipartParseOnce *sync.Once
 }
 
 // NewIncomingRequest creates an IncomingRequest
@@ -47,11 +50,15 @@ func NewIncomingRequest(req *http.Request) *IncomingRequest {
 	if req == nil {
 		return nil
 	}
+	req = req.WithContext(context.WithValue(req.Context(),
+		flightValuesCtxKey{}, flightValues{m: make(map[interface{}]interface{})}))
 	return &IncomingRequest{
-		req:    req,
-		Header: NewHeader(req.Header),
-		TLS:    req.TLS,
-		URL:    &URL{url: req.URL},
+		req:                req,
+		Header:             NewHeader(req.Header),
+		TLS:                req.TLS,
+		URL:                &URL{url: req.URL},
+		postParseOnce:      &sync.Once{},
+		multipartParseOnce: &sync.Once{},
 	}
 }
 
@@ -160,11 +167,13 @@ func (r *IncomingRequest) Context() context.Context {
 	return r.req.Context()
 }
 
-// SetContext sets the context of the safehttp.IncomingRequest to ctx. The
-// provided context must be non-nil, otherwise the method will panic.
-func (r *IncomingRequest) SetContext(ctx context.Context) {
-	if ctx == nil {
-		panic("nil context")
-	}
-	r.req = r.req.WithContext(ctx)
+// WithContext returns a shallow copy of the request with its context changed to
+// ctx. The provided ctx must be non-nil.
+//
+// This is similar to the net/http.Request.WithContext method.
+func (r *IncomingRequest) WithContext(ctx context.Context) *IncomingRequest {
+	r2 := new(IncomingRequest)
+	*r2 = *r
+	r2.req = r2.req.WithContext(ctx)
+	return r2
 }
diff --git a/safehttp/interceptor.go b/safehttp/interceptor.go
index fd814a8..0b734a3 100644
--- a/safehttp/interceptor.go
+++ b/safehttp/interceptor.go
@@ -19,6 +19,9 @@ package safehttp
 // See the documentation for ServeMux.ServeHTTP to understand how interceptors
 // are run, what happens in case of errors during request processing (i.e. which
 // interceptor methods are guaranteed to be run) etc.
+//
+// Interceptors keep their state across many requests and their methods can be
+// called concurrently. If you need per-request state, use FlightValues.
 type Interceptor interface {
 	// Before runs before the IncomingRequest is sent to the handler. If a
 	// response is written to the ResponseWriter, then the remaining
diff --git a/safehttp/plugins/csp/csp.go b/safehttp/plugins/csp/csp.go
index 1b3840e..503c39f 100644
--- a/safehttp/plugins/csp/csp.go
+++ b/safehttp/plugins/csp/csp.go
@@ -58,12 +58,12 @@ type Policy interface {
 	Serialize(nonce string) string
 }
 
-type ctxKey struct{}
+type nonceKey struct{}
 
 // Nonce retrieves the nonce from the given context. If there is no nonce stored
 // in the context, an error will be returned.
 func Nonce(ctx context.Context) (string, error) {
-	v := ctx.Value(ctxKey{})
+	v := safehttp.FlightValues(ctx).Get(nonceKey{})
 	if v == nil {
 		return "", errors.New("no nonce in context")
 	}
@@ -248,7 +248,7 @@ func Default(reportURI string) Interceptor {
 // Content-Security-Policy-Report-Only header.
 func (it Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
 	nonce := generateNonce()
-	r.SetContext(context.WithValue(r.Context(), ctxKey{}, nonce))
+	safehttp.FlightValues(r.Context()).Put(nonceKey{}, nonce)
 
 	var CSPs []string
 	for _, p := range it.Enforce {


Test patch:
diff --git a/safehttp/flightvalues_test.go b/safehttp/flightvalues_test.go
new file mode 100644
index 0000000..8b468b7
--- /dev/null
+++ b/safehttp/flightvalues_test.go
@@ -0,0 +1,104 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttp_test
+
+import (
+	"context"
+	"fmt"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+)
+
+type safeHeadersInterceptor struct{}
+
+func (ip *safeHeadersInterceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg safehttp.InterceptorConfig) safehttp.Result {
+	// We claim the header here in order to protect it from being tampered. The
+	// only way to set it is through a helper method exposed by this package. It
+	// only allows for setting safe values.
+	setter := w.Header().Claim("Super-Safe-Header")
+	safehttp.FlightValues(r.Context()).Put(safeHeaderKey{}, setter)
+	return safehttp.NotWritten()
+}
+
+func (ip *safeHeadersInterceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg safehttp.InterceptorConfig) {
+}
+
+func (ip *safeHeadersInterceptor) Match(_ safehttp.InterceptorConfig) bool {
+	// This interceptor does not offer any configuration options.
+	return false
+}
+
+type safeHeaderKey struct{}
+
+func SetHeaderSafely(ctx context.Context, level int) {
+	var value string
+	switch level {
+	case 0:
+		value = "Safe"
+	case 1:
+		value = "VerySafe"
+	case 2:
+		value = "VeryVerySafe"
+	default:
+		value = "Safe"
+	}
+	setter := safehttp.FlightValues(ctx).Get(safeHeaderKey{}).(func([]string))
+	setter([]string{value})
+}
+
+func handlerInteractingWithTheInterceptor(w safehttp.ResponseWriter, req *safehttp.IncomingRequest) safehttp.Result {
+	f, err := req.URL.Query()
+	if err != nil {
+		panic(err)
+	}
+	safety := f.Int64("level", 0)
+	SetHeaderSafely(req.Context(), int(safety))
+
+	return w.Write(safehtml.HTMLEscaped(fmt.Sprintf("Safety header set to %v", safety)))
+}
+
+func TestHandlerInteractingWithInterceptor(t *testing.T) {
+	mb := safehttp.NewServeMuxConfig(nil)
+	mb.Intercept(&safeHeadersInterceptor{})
+
+	mb.Handle("/safety", safehttp.MethodGet, safehttp.HandlerFunc(handlerInteractingWithTheInterceptor))
+
+	m := mb.Mux()
+	rr := httptest.NewRecorder()
+
+	req := httptest.NewRequest(safehttp.MethodGet, "https://foo.com/safety?level=2", nil)
+	m.ServeHTTP(rr, req)
+
+	if got, want := rr.Code, safehttp.StatusOK; got != int(want) {
+		t.Errorf("rr.Code got: %v want: %v", got, want)
+	}
+
+	want := `Safety header set to 2`
+	if diff := cmp.Diff(want, rr.Body.String()); diff != "" {
+		t.Errorf("response body diff (-want,+got): \n%s\ngot %q, want %q", diff, rr.Body.String(), want)
+	}
+
+	wantHeaders := map[string][]string{
+		"Content-Type":      {"text/html; charset=utf-8"},
+		"Super-Safe-Header": {"VeryVerySafe"},
+	}
+	if diff := cmp.Diff(wantHeaders, map[string][]string(rr.Header())); diff != "" {
+		t.Errorf("rr.Header mismatch (-want +got):\n%s", diff)
+	}
+}
diff --git a/safehttp/incoming_request_test.go b/safehttp/incoming_request_test.go
index f2fd6f1..4862922 100644
--- a/safehttp/incoming_request_test.go
+++ b/safehttp/incoming_request_test.go
@@ -148,7 +148,7 @@ type pizza struct {
 
 type pizzaKey string
 
-func TestRequestSetValidContextWithValue(t *testing.T) {
+func TestRequestWithContext(t *testing.T) {
 	tests := []struct {
 		name    string
 		key     pizzaKey
@@ -171,8 +171,7 @@ func TestRequestSetValidContextWithValue(t *testing.T) {
 	for _, test := range tests {
 		req := httptest.NewRequest(safehttp.MethodGet, "/", nil)
 		ir := safehttp.NewIncomingRequest(req)
-		ctx := context.WithValue(ir.Context(), pizzaKey("1234"), &pizza{val: "margeritta"})
-		ir.SetContext(ctx)
+		ir = ir.WithContext(context.WithValue(ir.Context(), pizzaKey("1234"), &pizza{val: "margeritta"}))
 
 		got, ok := ir.Context().Value(test.key).(*pizza)
 		if ok != test.wantOk {
@@ -198,7 +197,7 @@ func TestRequestSetNilContext(t *testing.T) {
 	// Avoids a linter complaint about a nil context being passed as argument.
 	// In this case, we explicitly want to test that a nil context results in an error.
 	var nilContext context.Context
-	ir.SetContext(nilContext)
+	ir.WithContext(nilContext)
 }
 
 func TestIncomingRequestPostForm(t *testing.T) {
diff --git a/safehttp/mux_test.go b/safehttp/mux_test.go
index 2401d76..9a99a0b 100644
--- a/safehttp/mux_test.go
+++ b/safehttp/mux_test.go
@@ -15,7 +15,6 @@
 package safehttp_test
 
 import (
-	"context"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -234,11 +233,11 @@ type claimHeaderInterceptor struct {
 	headerToClaim string
 }
 
-type claimCtxKey struct{}
+type claimKey struct{}
 
 func (p *claimHeaderInterceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg safehttp.InterceptorConfig) safehttp.Result {
 	f := w.Header().Claim(p.headerToClaim)
-	r.SetContext(context.WithValue(r.Context(), claimCtxKey{}, f))
+	safehttp.FlightValues(r.Context()).Put(claimKey{}, f)
 	return safehttp.NotWritten()
 }
 
@@ -250,7 +249,7 @@ func (claimHeaderInterceptor) Match(safehttp.InterceptorConfig) bool {
 }
 
 func claimInterceptorSetHeader(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, value string) {
-	f := r.Context().Value(claimCtxKey{}).(func([]string))
+	f := safehttp.FlightValues(r.Context()).Get(claimKey{}).(func([]string))
 	f([]string{value})
 }
 
diff --git a/safehttp/plugins/csp/csp_test.go b/safehttp/plugins/csp/csp_test.go
index 5984a75..ef1bf9d 100644
--- a/safehttp/plugins/csp/csp_test.go
+++ b/safehttp/plugins/csp/csp_test.go
@@ -15,7 +15,6 @@
 package csp
 
 import (
-	"context"
 	"errors"
 	"os"
 	"testing"
@@ -207,9 +206,9 @@ func TestBefore(t *testing.T) {
 				t.Errorf("h.Values(\"Content-Security-Policy-Report-Only\") mismatch (-want +got):\n%s", diff)
 			}
 
-			v := req.Context().Value(ctxKey{})
+			v := safehttp.FlightValues(req.Context()).Get(nonceKey{})
 			if v == nil {
-				t.Fatalf("req.Context().Value(ctxKey{}) got: nil want: %q", tt.wantNonce)
+				t.Fatalf("FlightValues(...).Get(nonceKey{}) got: nil want: %q", tt.wantNonce)
 			}
 			if got := v.(string); got != tt.wantNonce {
 				t.Errorf("v.(string) got: %q want: %q", got, tt.wantNonce)
@@ -240,8 +239,9 @@ func TestPanicWhileGeneratingNonce(t *testing.T) {
 }
 
 func TestValidNonce(t *testing.T) {
-	ctx := context.WithValue(context.Background(), ctxKey{}, "nonce")
-	n, err := Nonce(ctx)
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+	safehttp.FlightValues(req.Context()).Put(nonceKey{}, "nonce")
+	n, err := Nonce(req.Context())
 	if err != nil {
 		t.Errorf("Nonce(ctx) got err: %v want: nil", err)
 	}
@@ -252,8 +252,9 @@ func TestValidNonce(t *testing.T) {
 }
 
 func TestNonceEmptyContext(t *testing.T) {
-	ctx := context.Background()
-	n, err := Nonce(ctx)
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+	safehttp.FlightValues(req.Context()) // not putting nonce
+	n, err := Nonce(req.Context())
 	if err == nil {
 		t.Error("Nonce(ctx) got err: nil want: error")
 	}
@@ -266,7 +267,7 @@ func TestNonceEmptyContext(t *testing.T) {
 func TestCommitNonce(t *testing.T) {
 	fakeRW, rr := safehttptest.NewFakeResponseWriter()
 	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
-	req.SetContext(context.WithValue(req.Context(), ctxKey{}, "pizza"))
+	safehttp.FlightValues(req.Context()).Put(nonceKey{}, "pizza")
 
 	it := Interceptor{}
 	tr := &safehttp.TemplateResponse{}
@@ -301,7 +302,7 @@ func TestCommitNonce(t *testing.T) {
 func TestCommitMissingNonce(t *testing.T) {
 	fakeRW, _ := safehttptest.NewFakeResponseWriter()
 	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
-	req.SetContext(context.Background())
+	safehttp.FlightValues(req.Context()).Put(nonceKey{}, nil /* missing nonce */)
 
 	it := Interceptor{}
 	tr := &safehttp.TemplateResponse{}