Skip to content

Commit d55ab6b

Browse files
authored
[TT-17921] Improve header handling for GraphQL-based APIs (#8602)
## Summary Values that belong to a single request — the round tripper, the resolved upstream headers, the upstream response — were being written into, or keyed on, state that outlives the request: the shared per-API `http.Client`, the plan cache, and the upstream connection-reuse key. Under concurrency the result was not deterministic: which request's values a fetch used depended on timing. This makes that state request-scoped, and fixes the header handling that depended on it. Seven commits, each independently revertable. `internal/graphengine` goes from 22 to 76 test functions (273 including subtests), green under `-race -count=10`. Behavioural detail and reproduction steps are on the ticket. ## What changed ### scope transports to requests `reverseProxyPreHandlerV1/V2.PreHandle` assigned `params.RoundTripper` onto the per-API `httpClient.Transport` on every request, so concurrent requests overwrote each other's. The round tripper and headers config now travel in the request context (`SetGraphQLEngineTransportContextValue`), and `GraphQLEngineTransport.RoundTrip` builds a per-request view from them, falling back to the client's own transport. ### keep subscriptions alive, isolate proxy-only responses Three defects, two of them introduced by the commit above: - **WebSocket subscriptions never delivered.** The handovers passed `WithContext(params.OutRequest.Context())`, but `net/http` calls `w.cancelCtx()` as soon as `ServeHTTP` returns — before the hijacked check in `conn.serve` — and the gateway returns as soon as the connection is hijacked. The subscription context was cancelled before anything was sent. `subscriptionRequestContext` roots the subscription at the long-lived engine spec context while carrying the opening request's transport values across. EngineV1 passes no context: its legacy data sources drop it, so inheriting the request's cancellation could only break the subscription. - **`handleProxyOnly` rewrote the outgoing method**, turning a proxy-only upstream WebSocket handshake into a POST, which WebSocket servers reject. Handshakes now stay `GET`. - **`GraphQLProxyOnlyContextValues.upstreamResponse` was unsynchronised.** The transport writes it on the resolver's trigger goroutine while the engine reads it (~1 in 20 under `-race`). Now behind a mutex. ### cover the request isolation matrix `credential_isolation_test.go`: proxy-only, UDG and supergraph × HTTP, SSE, `graphql-ws` and `graphql-transport-ws` × EngineV2 and EngineV3. Each test drives more than one request through one engine and asserts what each upstream connection actually received, with negative controls (`strip_auth_data`, a custom auth header name, a request that sets no header after one that did), 20-request concurrent bursts, a per-request round tripper test that shared mutable transport state cannot pass, and the client WebSocket-upgrade path that the regression above had slipped through. ### move to the fixed graphql-go-tools, cover the cold path Both module pins move to the library branch; `coder/websocket` becomes a direct dependency. Two races are fixed upstream — v1 `Schema.Hash` writing `s.hash` on first use, and v2 `ResolveGraphQLResponse` writing `Info` on the cached plan — so the cold-start concurrent burst becomes permanent coverage instead of something a warm-up had to work around. ### resolve dynamic upstream headers before the fetch is keyed The v1 header modifier merged Tyk's additional headers and stopped, leaving `tykVariableReplacer` injected but never read, so `$tyk_context.*` in an upstream header reached the fetch unresolved on config version 2. That matters for more than the value that arrives upstream: the library applies the modifier before it derives the key that groups equivalent subscriptions onto one upstream connection (`connectionKey` in the v1 subscription client, `UniqueRequestID` in the v2 resolver). An unresolved template is the same string for every request, so requests that should have been distinct were treated as equivalent and pooled together. Resolving there is only correct now that the library moved header modification to the execution phase. TT-14357 (`adca34d46`) had removed it because the modifier ran at plan post-process time and the result was cached into the plan, and added `variableReplaceRoundTripper` as the workaround. That wrapper is retired here: it ran after the grouping key was derived, it collapsed multi-value headers via `Get`/`Set` on the way to the wire, and it expanded variables in header values that did not come from the API definition. Its one remaining job, `request_headers_rewrite`, is resolved once where the rules are built, from configuration only. ### restore the gateway round tripper for EngineV1 fetches Regression from `cf506a971`, caught while verifying the above: `TestGraphQL_InternalDataSource/graphql_engine_v1` fails at the branch tip and passes at the base — a config version 1 API with a `tyk://` data source answers 500. The transport assignment that commit removed was also the only thing giving EngineV1 the gateway round tripper, and only that round tripper can route `tyk://`. Since EngineV1's data sources drop the execution context, the round tripper is handed over as a mutex-guarded transport-level fallback, used only when a request carries none of its own. EngineV2 and EngineV3 stay fully request-scoped. That fallback is the one piece of transport state shared between requests, and it is only sound because the round tripper the gateway hands over no longer carries request-specific state — the wrapper retired in `4d67f9af4` did. **The two commits must not be reverted independently**; both sides carry comments saying so. ### forward a consumer header to the upstream once Two writers put the consumer's headers on the upstream request and neither knew about the other: `propagateAuthHeaders` adds them to the fetch input when `strip_auth_data` is off, and `setProxyOnlyHeaders` then forwards every consumer header again with `Header.Add`. With `use_immutable_headers` off nothing removed the first copy, so a header present in both arrived duplicated. It went unnoticed because the wrapper retired in `4d67f9af4` had been collapsing repeated values with `Get`/`Set` since before the second writer was added. `setProxyOnlyHeaders` now skips a value the engine already placed there, compared against a snapshot taken before forwarding so a consumer that genuinely sent the same value twice keeps both. It also hoists the `use_immutable_headers` delete out of the value loop, where it removed the value added on the previous pass and left a multi-value consumer header holding only its last value. ## Behaviour changes worth a reviewer's sign-off 1. **An API reload or release now ends that API's subscriptions.** Previously they ran on `context.Background()` and outlived the reload. 2. **No header value is resolved twice** on config version 2 or 3-preview. Values that do not come from the API definition are no longer expanded, and a value that resolved into another template token no longer gets a second pass. 3. **An exact overlap between a configured `request_headers` value and a consumer value now reaches the upstream once**, not twice. Differing values still both go upstream — TT-11990 / TT-12190 semantics are unchanged. 4. **Multi-value headers survive to the upstream.** `Accept-Encoding: gzip, deflate, br` used to arrive as `gzip` alone. <!---TykTechnologies/jira-linter starts here--> ### Ticket Details <details> <summary> <a href="https://tyktech.atlassian.net/browse/TT-17921" title="TT-17921" target="_blank">TT-17921</a> </summary> | | | |---------|----| | Status | Merge | | Summary | Federated supergraph reuses first caller’s JWT across users and can share HTTP responses via single-flight | Generated at: 2026-08-20 13:03:45 </details> <!---TykTechnologies/jira-linter ends here-->
1 parent 593a1a1 commit d55ab6b

22 files changed

Lines changed: 3068 additions & 75 deletions

gateway/reverse_proxy.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,32 +1089,28 @@ func isCORSPreflight(r *http.Request) bool {
10891089
return r.Method == http.MethodOptions
10901090
}
10911091

1092-
type variableReplaceRoundTripper struct {
1093-
next http.RoundTripper
1094-
outReq *http.Request
1095-
gw *Gateway
1096-
}
1097-
1098-
func (d *variableReplaceRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
1099-
for key := range req.Header {
1100-
val := d.gw.ReplaceTykVariables(d.outReq, req.Header.Get(key), false)
1101-
req.Header.Set(key, val)
1102-
}
1103-
1104-
return d.next.RoundTrip(req)
1105-
}
1106-
11071092
func (p *ReverseProxy) handleGraphQL(roundTripper *TykRoundTripper, outreq *http.Request, w http.ResponseWriter) (res *http.Response, hijacked bool, err error) {
11081093
isWebSocketUpgrade := ctxGetGraphQLIsWebSocketUpgrade(outreq)
11091094
needsEngine := needsGraphQLExecutionEngine(p.TykAPISpec)
11101095

11111096
requestHeadersRewrite := make(map[string]apidef.RequestHeadersRewriteConfig)
11121097
for key, value := range p.TykAPISpec.GraphQL.Proxy.RequestHeadersRewrite {
1098+
// Resolved here, once, because these are the only header values the graph engine
1099+
// adds that no header modifier sees: the transport applies them itself, after the
1100+
// engine has already finalised the fetch headers.
1101+
//
1102+
// Nothing else on this path resolves variables any more. Upstream headers of the
1103+
// engine, global and per data source alike, are resolved by the header modifier
1104+
// while the fetch input is built, which is what the subscription connection key is
1105+
// computed from (TT-17921). A round tripper that walked the outgoing headers
1106+
// instead used to sit here, and it ran too late for that key, collapsed multi
1107+
// value headers and expanded variables inside header values the caller had sent.
1108+
value.Value = p.Gw.ReplaceTykVariables(outreq, value.Value, false)
11131109
// Use the canonical format of the MIME header key.
11141110
requestHeadersRewrite[textproto.CanonicalMIMEHeaderKey(key)] = value
11151111
}
11161112
res, hijacked, err = p.TykAPISpec.GraphEngine.HandleReverseProxy(graphengine.ReverseProxyParams{
1117-
RoundTripper: &variableReplaceRoundTripper{next: roundTripper, outReq: outreq, gw: p.Gw},
1113+
RoundTripper: roundTripper,
11181114
ResponseWriter: w,
11191115
OutRequest: outreq,
11201116
WebSocketUpgrader: &p.wsUpgrader,

gateway/reverse_proxy_test.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,13 @@ func TestGraphQL_UDGHeaders(t *testing.T) {
11161116
strings.Contains(string(b), `{"name":"Context","value":"request-context"}`) &&
11171117
strings.Contains(string(b), `{"name":"Global-Static","value":"foobar"}`) &&
11181118
strings.Contains(string(b), `{"name":"Global-Context","value":"follow-up-request-global-context"}`) &&
1119-
strings.Contains(string(b), `{"name":"Does-Exist-Already","value":"ds-does-exist-already"}`)
1119+
strings.Contains(string(b), `{"name":"Does-Exist-Already","value":"ds-does-exist-already"}`) &&
1120+
// A header with more than one value has to keep all of them. The round
1121+
// tripper that used to resolve variables on the way to the upstream read
1122+
// with Get and wrote with Set, so it collapsed this to gzip alone.
1123+
strings.Contains(string(b), `{"name":"Accept-Encoding","value":"gzip"}`) &&
1124+
strings.Contains(string(b), `{"name":"Accept-Encoding","value":"deflate"}`) &&
1125+
strings.Contains(string(b), `{"name":"Accept-Encoding","value":"br"}`)
11201126
},
11211127
},
11221128
}...)
@@ -1195,6 +1201,107 @@ func TestGraphQL_ProxyOnlyHeaders(t *testing.T) {
11951201
})
11961202
assert.NoError(t, err)
11971203
})
1204+
1205+
t.Run("test context variable request headers rewrite", func(t *testing.T) {
1206+
// request_headers_rewrite is applied by the engine transport, after the header
1207+
// modifier has already finalised the fetch headers, so it is resolved where the
1208+
// rules are built instead. See handleGraphQL.
1209+
spec := defaultSpec
1210+
spec.GraphQL.Proxy.RequestHeadersRewrite = map[string]apidef.RequestHeadersRewriteConfig{
1211+
"X-Rewritten": {Value: "$tyk_context.headers_Test_Header"},
1212+
}
1213+
spec.EnableContextVars = true
1214+
g.Gw.LoadAPI(spec)
1215+
g.AddDynamicHandler("/dynamic", func(writer http.ResponseWriter, r *http.Request) {
1216+
if !headerCheck("X-Rewritten", "test-value", r.Header) {
1217+
t.Errorf("rewritten header not resolved, got %q", r.Header.Get("X-Rewritten"))
1218+
}
1219+
})
1220+
_, err := g.Run(t, test.TestCase{
1221+
Path: "/",
1222+
Headers: map[string]string{
1223+
"Test-Header": "test-value",
1224+
},
1225+
Method: http.MethodPost,
1226+
Data: graphql.Request{
1227+
Query: gqlContinentQuery,
1228+
},
1229+
})
1230+
assert.NoError(t, err)
1231+
})
1232+
1233+
t.Run("the consumer's credential reaches the upstream once", func(t *testing.T) {
1234+
// Two writers put it there and neither knows about the other: with strip_auth_data
1235+
// off the engine adds the consumer's auth header to the fetch input through
1236+
// propagateAuthHeaders, and setProxyOnlyHeaders then forwards the consumer's
1237+
// headers again. The upstream used to receive the credential twice.
1238+
spec := defaultSpec
1239+
spec.GraphQL.Proxy.RequestHeadersRewrite = nil
1240+
spec.UseKeylessAccess = false
1241+
spec.UseStandardAuth = true
1242+
spec.StripAuthData = false
1243+
spec.AuthConfigs = map[string]apidef.AuthConfig{
1244+
apidef.AuthTokenType: {AuthHeaderName: "X-API-KEY"},
1245+
}
1246+
g.Gw.LoadAPI(spec)
1247+
1248+
_, authKey := g.CreateSession(func(s *user.SessionState) {
1249+
s.AccessRights = map[string]user.AccessDefinition{
1250+
spec.APIID: {APIName: spec.Name, APIID: spec.APIID, Versions: []string{"Default"}},
1251+
}
1252+
s.OrgID = spec.OrgID
1253+
})
1254+
1255+
g.AddDynamicHandler("/dynamic", func(writer http.ResponseWriter, r *http.Request) {
1256+
values := r.Header.Values("X-Api-Key")
1257+
if len(values) != 1 {
1258+
t.Errorf("upstream received X-Api-Key %d times: %v", len(values), values)
1259+
return
1260+
}
1261+
if values[0] != authKey {
1262+
t.Errorf("upstream received the wrong credential: %q", values[0])
1263+
}
1264+
})
1265+
_, err := g.Run(t, test.TestCase{
1266+
Path: "/",
1267+
Headers: map[string]string{
1268+
"X-API-KEY": authKey,
1269+
},
1270+
Method: http.MethodPost,
1271+
Data: graphql.Request{
1272+
Query: gqlContinentQuery,
1273+
},
1274+
})
1275+
assert.NoError(t, err)
1276+
})
1277+
1278+
t.Run("a variable inside a header the caller sent is not expanded", func(t *testing.T) {
1279+
// Only values that come from the API definition are resolved. A round tripper that
1280+
// walked every outgoing header used to sit on this path and expanded whatever the
1281+
// caller had put in one, which let a caller read the context of their own session
1282+
// back out of the upstream request. See handleGraphQL.
1283+
spec := defaultSpec
1284+
spec.GraphQL.Proxy.RequestHeadersRewrite = nil
1285+
spec.EnableContextVars = true
1286+
g.Gw.LoadAPI(spec)
1287+
g.AddDynamicHandler("/dynamic", func(writer http.ResponseWriter, r *http.Request) {
1288+
if !headerCheck("X-Injection-Probe", "$tyk_context.headers_Test_Header", r.Header) {
1289+
t.Errorf("caller supplied variable was expanded, got %q", r.Header.Get("X-Injection-Probe"))
1290+
}
1291+
})
1292+
_, err := g.Run(t, test.TestCase{
1293+
Path: "/",
1294+
Headers: map[string]string{
1295+
"Test-Header": "test-value",
1296+
"X-Injection-Probe": "$tyk_context.headers_Test_Header",
1297+
},
1298+
Method: http.MethodPost,
1299+
Data: graphql.Request{
1300+
Query: gqlContinentQuery,
1301+
},
1302+
})
1303+
assert.NoError(t, err)
1304+
})
11981305
}
11991306

12001307
func TestGraphQL_ProxyOnlyPassHeadersWithOTel(t *testing.T) {

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/TykTechnologies/goautosocket v0.0.0-20190430121222-97bfa5e7e481
2626
github.com/TykTechnologies/gorpc v0.0.0-20250214161245-e9f3f088e8c6
2727
github.com/TykTechnologies/goverify v0.0.0-20260203113354-7a104729566e
28-
github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20260624141309-dae0d8d8d038
28+
github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20260818151404-c416c52a2dd3
2929
github.com/TykTechnologies/graphql-translator v0.0.0-20250602105400-41c2e7514a36
3030
github.com/TykTechnologies/murmur3 v0.0.0-20230310161213-aad17efd5632
3131
github.com/TykTechnologies/openid2go v0.1.2
@@ -97,10 +97,11 @@ require (
9797
github.com/Azure/go-amqp v1.4.0
9898
github.com/IBM/sarama v1.46.3
9999
github.com/Jeffail/gabs/v2 v2.7.0
100-
github.com/TykTechnologies/graphql-go-tools/v2 v2.0.0-20250926102005-c54e73aae17d
100+
github.com/TykTechnologies/graphql-go-tools/v2 v2.0.0-20260818151404-c416c52a2dd3
101101
github.com/TykTechnologies/opentelemetry v0.0.26
102102
github.com/TykTechnologies/structviewer v1.2.0
103103
github.com/alecthomas/kingpin/v2 v2.4.0
104+
github.com/coder/websocket v1.8.13
104105
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd
105106
github.com/eclipse/paho.mqtt.golang v1.5.1
106107
github.com/getkin/kin-openapi v0.133.0
@@ -270,7 +271,6 @@ require (
270271
github.com/clbanning/mxj/v2 v2.7.0 // indirect
271272
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
272273
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
273-
github.com/coder/websocket v1.8.13 // indirect
274274
github.com/colinmarc/hdfs v1.1.3 // indirect
275275
github.com/containerd/continuity v0.4.2 // indirect
276276
github.com/containerd/errdefs v1.0.0 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,10 @@ github.com/TykTechnologies/gorpc v0.0.0-20250214161245-e9f3f088e8c6 h1:wwt23wdyi
177177
github.com/TykTechnologies/gorpc v0.0.0-20250214161245-e9f3f088e8c6/go.mod h1:v6v7Mlj08+EmEcXOfpuTxGt2qYU9yhqqtv4QF9Wf50E=
178178
github.com/TykTechnologies/goverify v0.0.0-20260203113354-7a104729566e h1:wbd35YYCywZbgc4Fk/G2pQHnvwYzjedrw8pNzUsVowg=
179179
github.com/TykTechnologies/goverify v0.0.0-20260203113354-7a104729566e/go.mod h1:WtiSLIlItUVsHyHQB1NG+de/L4/PTtmZWc9CnzTRBLs=
180-
github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20260624141309-dae0d8d8d038 h1:akyeFvwB5ZnVq9jbGPx1pP33ETxp0ziDlt5r40HRCDY=
181-
github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20260624141309-dae0d8d8d038/go.mod h1:bvVafmGebtdjIFG2bXkLd+O1jOjjU/3To+mQHcLr4KI=
182-
github.com/TykTechnologies/graphql-go-tools/v2 v2.0.0-20250926102005-c54e73aae17d h1:bK9T78hExbTuDK4UaBuGi9aL28hK68Uw3OyNZswdPcA=
183-
github.com/TykTechnologies/graphql-go-tools/v2 v2.0.0-20250926102005-c54e73aae17d/go.mod h1:XM1owY0ZCJ1Rai64Q1UKXZYNDkWikZDojgefZw8raPk=
180+
github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20260818151404-c416c52a2dd3 h1:muQy5S8Y0bZk3wNLQnHjDJzUBB5I3f5FzftIkpCIP5s=
181+
github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20260818151404-c416c52a2dd3/go.mod h1:bvVafmGebtdjIFG2bXkLd+O1jOjjU/3To+mQHcLr4KI=
182+
github.com/TykTechnologies/graphql-go-tools/v2 v2.0.0-20260818151404-c416c52a2dd3 h1:R7ZoRYvA/rEJj648PWCRK2q6wHtEO/oDp22kRCDtdF0=
183+
github.com/TykTechnologies/graphql-go-tools/v2 v2.0.0-20260818151404-c416c52a2dd3/go.mod h1:XM1owY0ZCJ1Rai64Q1UKXZYNDkWikZDojgefZw8raPk=
184184
github.com/TykTechnologies/graphql-translator v0.0.0-20250602105400-41c2e7514a36 h1:7nNsyocI/RKBqo73RR9G/SiFMZ8w2sN+HMsQXYp9wPI=
185185
github.com/TykTechnologies/graphql-translator v0.0.0-20250602105400-41c2e7514a36/go.mod h1:qiglVaUPPOWET4bQsBmsAaLiCml20a01REQSi7TSUa0=
186186
github.com/TykTechnologies/kin-openapi v0.512.1-0.20260817104659-7198d52254ff h1:lpF67zYIQzUMacE5UraBYrzaajVEkHO3h/TzCNWSCwY=

internal/graphengine/context.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package graphengine
33
import (
44
"context"
55
"net/http"
6+
"sync"
67

78
"github.com/TykTechnologies/tyk/apidef"
89
)
@@ -16,12 +17,80 @@ const (
1617

1718
type contextKey struct{}
1819

20+
type transportContextKey struct{}
21+
1922
var graphqlProxyContextInfo = contextKey{}
23+
var graphqlTransportContextInfo = transportContextKey{}
24+
25+
type graphQLEngineTransportContextValues struct {
26+
roundTripper http.RoundTripper
27+
headersConfig ReverseProxyHeadersConfig
28+
}
29+
30+
func SetGraphQLEngineTransportContextValue(ctx context.Context, roundTripper http.RoundTripper, headersConfig ReverseProxyHeadersConfig) context.Context {
31+
value := &graphQLEngineTransportContextValues{
32+
roundTripper: roundTripper,
33+
headersConfig: headersConfig,
34+
}
35+
return context.WithValue(ctx, graphqlTransportContextInfo, value)
36+
}
37+
38+
func copyGraphQLEngineTransportContextValue(target, source context.Context) context.Context {
39+
value, ok := source.Value(graphqlTransportContextInfo).(*graphQLEngineTransportContextValues)
40+
if !ok || value == nil {
41+
return target
42+
}
43+
return context.WithValue(target, graphqlTransportContextInfo, value)
44+
}
45+
46+
func getGraphQLEngineTransportContextValue(ctx context.Context) *graphQLEngineTransportContextValues {
47+
value, ok := ctx.Value(graphqlTransportContextInfo).(*graphQLEngineTransportContextValues)
48+
if !ok {
49+
return nil
50+
}
51+
return value
52+
}
53+
54+
// subscriptionRequestContext returns the context that a websocket subscription handler
55+
// runs on. It carries the transport values of the request that opened the connection, so
56+
// upstream fetches keep using that caller's round tripper, but it is rooted at the long
57+
// lived engine context so that releasing the API ends the subscription.
58+
//
59+
// The request context cannot be used as the parent: net/http cancels it as soon as
60+
// ServeHTTP returns, and the gateway returns as soon as the connection is hijacked, so a
61+
// subscription rooted there is cancelled before it delivers anything.
62+
func subscriptionRequestContext(engineCtx context.Context, outreq *http.Request) context.Context {
63+
if engineCtx == nil {
64+
engineCtx = context.Background()
65+
}
66+
if outreq == nil {
67+
return engineCtx
68+
}
69+
return copyGraphQLEngineTransportContextValue(engineCtx, outreq.Context())
70+
}
2071

2172
type GraphQLProxyOnlyContextValues struct {
2273
forwardedRequest *http.Request
23-
upstreamResponse *http.Response
2474
ignoreForwardedHeaders map[string]bool
75+
76+
// upstreamResponse is written by the transport and read by the engine. Those can be
77+
// different goroutines: a subscription fetch runs on the resolver's trigger goroutine
78+
// and can outlive the handover that reads the response, so both sides go through the
79+
// accessors below.
80+
mu sync.Mutex
81+
upstreamResponse *http.Response
82+
}
83+
84+
func (g *GraphQLProxyOnlyContextValues) setUpstreamResponse(response *http.Response) {
85+
g.mu.Lock()
86+
defer g.mu.Unlock()
87+
g.upstreamResponse = response
88+
}
89+
90+
func (g *GraphQLProxyOnlyContextValues) getUpstreamResponse() *http.Response {
91+
g.mu.Lock()
92+
defer g.mu.Unlock()
93+
return g.upstreamResponse
2594
}
2695

2796
func SetProxyOnlyContextValue(ctx context.Context, req *http.Request) context.Context {

0 commit comments

Comments
 (0)