Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-10909]: fix issue with missing upstream headers in graphql proxy only #6166

Merged
merged 6 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ spec:
trigger:
type: http
httpRequest:
method: GET
method: POST
url: tyk:8080/test-graphql-tracing-invalid/test-graphql-tracing-invalid
body: "{\n \"query\": \"{\\n country(code: \\\"NG\\\"){\\n name\\n }\\n}\"\n}"
headers:
Expand Down
5 changes: 5 additions & 0 deletions gateway/handler_success_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/http"
"testing"

"github.com/TykTechnologies/graphql-go-tools/pkg/engine/datasource/httpclient"

"github.com/TykTechnologies/graphql-go-tools/pkg/graphql"
"github.com/TykTechnologies/tyk-pump/analytics"
"github.com/TykTechnologies/tyk/test"
Expand Down Expand Up @@ -228,6 +230,9 @@ func TestAnalyticRecord_GraphStats(t *testing.T) {
Data: tc.request,
Method: http.MethodPost,
Code: tc.code,
Headers: map[string]string{
httpclient.AcceptEncodingHeader: "",
},
})
assert.NoError(t, err)
})
Expand Down
38 changes: 38 additions & 0 deletions gateway/reverse_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,44 @@ func TestGraphQL_ProxyOnlyHeaders(t *testing.T) {
})
}

func TestGraphQL_ProxyOnlyPassHeadersWithOTel(t *testing.T) {
g := StartTest(func(globalConf *config.Config) {
globalConf.OpenTelemetry.Enabled = true
})
defer g.Close()

spec := BuildAPI(func(spec *APISpec) {
spec.Name = "tyk-api"
spec.APIID = "tyk-api"
spec.GraphQL.Enabled = true
spec.GraphQL.ExecutionMode = apidef.GraphQLExecutionModeProxyOnly
spec.GraphQL.Schema = gqlCountriesSchema
spec.GraphQL.Version = apidef.GraphQLConfigVersion2
spec.Proxy.TargetURL = TestHttpAny + "/dynamic"
spec.Proxy.ListenPath = "/"
})[0]

g.Gw.LoadAPI(spec)
g.AddDynamicHandler("/dynamic", func(writer http.ResponseWriter, r *http.Request) {
if gotten := r.Header.Get("custom-client-header"); gotten != "custom-value" {
t.Errorf("expected upstream to recieve header `custom-client-header` with value of `custom-value`, instead got %s", gotten)
}
})

_, err := g.Run(t, test.TestCase{
Path: "/",
Headers: map[string]string{
"custom-client-header": "custom-value",
},
Method: http.MethodPost,
Data: graphql.Request{
Query: gqlContinentQuery,
},
})

assert.NoError(t, err)
}

func TestGraphQL_InternalDataSource(t *testing.T) {
g := StartTest(nil)
defer g.Close()
Expand Down
31 changes: 31 additions & 0 deletions internal/graphengine/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,37 @@ const (
GraphQLEngineTransportTypeMultiUpstream
)

type contextKey struct{}

var graphqlProxyContextInfo = contextKey{}

type GraphQLProxyOnlyContextValues struct {
forwardedRequest *http.Request
upstreamResponse *http.Response
ignoreForwardedHeaders map[string]bool
}

func SetProxyOnlyContextValue(ctx context.Context, req *http.Request) context.Context {
value := &GraphQLProxyOnlyContextValues{
forwardedRequest: req,
ignoreForwardedHeaders: map[string]bool{
http.CanonicalHeaderKey("date"): true,
http.CanonicalHeaderKey("content-type"): true,
http.CanonicalHeaderKey("content-length"): true,
},
}

return context.WithValue(ctx, graphqlProxyContextInfo, value)
}

func GetProxyOnlyContextValue(ctx context.Context) *GraphQLProxyOnlyContextValues {
val, ok := ctx.Value(graphqlProxyContextInfo).(*GraphQLProxyOnlyContextValues)
if !ok {
return nil
}
return val
}

func DetermineGraphQLEngineTransportType(apiDefinition *apidef.APIDefinition) GraphQLEngineTransportType {
switch apiDefinition.GraphQL.ExecutionMode {
case apidef.GraphQLExecutionModeSubgraph:
Expand Down
40 changes: 37 additions & 3 deletions internal/graphengine/engine_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"errors"
"net/http"
"strings"

"github.com/TykTechnologies/graphql-go-tools/pkg/engine/datasource/httpclient"

"github.com/jensneuse/abstractlogger"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -262,7 +265,7 @@ func (e *EngineV2) handoverRequestToGraphQLExecutionEngine(gqlRequest *graphql.R
span := otel.SpanFromContext(outreq.Context())
reqCtx := otel.ContextWithSpan(context.Background(), span)
if isProxyOnly {
reqCtx = NewGraphQLProxyOnlyContext(reqCtx, outreq)
reqCtx = SetProxyOnlyContextValue(reqCtx, outreq)
}

resultWriter := graphql.NewEngineResultWriter()
Expand Down Expand Up @@ -290,7 +293,7 @@ func (e *EngineV2) handoverRequestToGraphQLExecutionEngine(gqlRequest *graphql.R
header.Set("Content-Type", "application/json")

if isProxyOnly {
proxyOnlyCtx := reqCtx.(*GraphQLProxyOnlyContext)
proxyOnlyCtx := GetProxyOnlyContextValue(reqCtx)
// There is a case in the proxy-only mode where the request can be handled
// by the library without calling the upstream.
// This is a valid query for proxy-only mode: query { __typename }
Expand All @@ -299,19 +302,50 @@ func (e *EngineV2) handoverRequestToGraphQLExecutionEngine(gqlRequest *graphql.R
if proxyOnlyCtx.upstreamResponse != nil {
header = proxyOnlyCtx.upstreamResponse.Header
httpStatus = proxyOnlyCtx.upstreamResponse.StatusCode
// change the value of the header's content encoding to use the content encoding defined by the accept encoding
contentEncoding := selectContentEncodingToBeUsed(proxyOnlyCtx.forwardedRequest.Header.Get(httpclient.AcceptEncodingHeader))
header.Set(httpclient.ContentEncodingHeader, contentEncoding)
if e.ApiDefinition.GraphQL.Proxy.UseResponseExtensions.OnErrorForwarding && httpStatus >= http.StatusBadRequest {
err = e.gqlTools.returnErrorsFromUpstream(proxyOnlyCtx, &resultWriter, e.seekReadCloser)
if err != nil {
return
}
}

}
}

res = resultWriter.AsHTTPResponse(httpStatus, header)
return
}

// selectContentEncodingToBeUsed selects the encoding value to be returned based on the IETF standards
// if acceptedEncoding is a list of comma separated strings br,gzip, deflate; then it selects the first supported one
// if it is a single value then it returns that value
// if no supported encoding is found, it returns the last value
func selectContentEncodingToBeUsed(acceptedEncoding string) string {
supportedHeaders := map[string]struct{}{
"gzip": {},
"deflate": {},
"br": {},
}

values := strings.Split(acceptedEncoding, ",")
if len(values) < 2 {
return values[0]
}

for i, e := range values {
enc := strings.TrimSpace(e)
if _, ok := supportedHeaders[enc]; ok {
return enc
}
if i == len(values)-1 {
return enc
}
}
return ""
}

func (e *EngineV2) handoverWebSocketConnectionToGraphQLExecutionEngine(params *ReverseProxyParams) (res *http.Response, hijacked bool, err error) {
conn, err := websocketConnWithUpgradeHeader(e.logger, params)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/graphengine/graphql_go_tools_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (g graphqlGoToolsV1) headerModifier(outreq *http.Request, additionalHeaders
}
}

func (g graphqlGoToolsV1) returnErrorsFromUpstream(proxyOnlyCtx *GraphQLProxyOnlyContext, resultWriter *graphql.EngineResultWriter, seekReadCloser SeekReadCloserFunc) error {
func (g graphqlGoToolsV1) returnErrorsFromUpstream(proxyOnlyCtx *GraphQLProxyOnlyContextValues, resultWriter *graphql.EngineResultWriter, seekReadCloser SeekReadCloserFunc) error {
body, err := seekReadCloser(proxyOnlyCtx.upstreamResponse.Body)
if body == nil {
// Response body already read by graphql-go-tools, and it's not re-readable. Quit silently.
Expand Down
20 changes: 10 additions & 10 deletions internal/graphengine/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ func NewGraphQLEngineTransport(transportType GraphQLEngineTransportType, origina
func (g *GraphQLEngineTransport) RoundTrip(request *http.Request) (res *http.Response, err error) {
switch g.transportType {
case GraphQLEngineTransportTypeProxyOnly:
proxyOnlyCtx, ok := request.Context().(*GraphQLProxyOnlyContext)
if ok {
return g.handleProxyOnly(proxyOnlyCtx, request)
val := GetProxyOnlyContextValue(request.Context())
if val != nil {
return g.handleProxyOnly(val, request)
}
}

return g.originalTransport.RoundTrip(request)
}

func (g *GraphQLEngineTransport) handleProxyOnly(proxyOnlyCtx *GraphQLProxyOnlyContext, request *http.Request) (*http.Response, error) {
request.Method = proxyOnlyCtx.forwardedRequest.Method
g.setProxyOnlyHeaders(proxyOnlyCtx, request)
func (g *GraphQLEngineTransport) handleProxyOnly(proxyOnlyValues *GraphQLProxyOnlyContextValues, request *http.Request) (*http.Response, error) {
request.Method = proxyOnlyValues.forwardedRequest.Method
g.setProxyOnlyHeaders(proxyOnlyValues, request)

response, err := g.originalTransport.RoundTrip(request)
if err != nil {
Expand All @@ -65,13 +65,13 @@ func (g *GraphQLEngineTransport) handleProxyOnly(proxyOnlyCtx *GraphQLProxyOnlyC
}
response.Body = reusableBody
}
proxyOnlyCtx.upstreamResponse = response
proxyOnlyValues.upstreamResponse = response
return response, err
}

func (g *GraphQLEngineTransport) setProxyOnlyHeaders(proxyOnlyCtx *GraphQLProxyOnlyContext, r *http.Request) {
for forwardedHeaderKey, forwardedHeaderValues := range proxyOnlyCtx.forwardedRequest.Header {
if proxyOnlyCtx.ignoreForwardedHeaders[forwardedHeaderKey] {
func (g *GraphQLEngineTransport) setProxyOnlyHeaders(proxyOnlyValues *GraphQLProxyOnlyContextValues, r *http.Request) {
for forwardedHeaderKey, forwardedHeaderValues := range proxyOnlyValues.forwardedRequest.Header {
if proxyOnlyValues.ignoreForwardedHeaders[forwardedHeaderKey] {
continue
}

Expand Down
Loading