Skip to content
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
111 changes: 70 additions & 41 deletions v2/pkg/engine/resolve/authorization_test.go

Large diffs are not rendered by default.

35 changes: 30 additions & 5 deletions v2/pkg/engine/resolve/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"errors"
"io"
"net/http"
"sort"
"time"

"github.com/wundergraph/astjson"

"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient"
)

// Context should not ever be initialized directly, and should be initialized via the NewContext function
type Context struct {
ctx context.Context
Variables *astjson.Value
Expand All @@ -31,7 +33,7 @@ type Context struct {
rateLimiter RateLimiter
fieldRenderer FieldValueRenderer

subgraphErrors error
subgraphErrors map[string]error
}

type ExecutionOptions struct {
Expand Down Expand Up @@ -138,11 +140,26 @@ func (c *Context) SetRateLimiter(limiter RateLimiter) {
}

func (c *Context) SubgraphErrors() error {
Comment thread
SkArchon marked this conversation as resolved.
return c.subgraphErrors
if len(c.subgraphErrors) == 0 {
return nil
}

// Ensure the errors are appended in an idempotent order
keys := make([]string, 0, len(c.subgraphErrors))
for k := range c.subgraphErrors {
keys = append(keys, k)
}
sort.Strings(keys)

var joined error
for _, k := range keys {
joined = errors.Join(joined, c.subgraphErrors[k])
}
return joined
Comment thread
SkArchon marked this conversation as resolved.
}

func (c *Context) appendSubgraphErrors(errs ...error) {
c.subgraphErrors = errors.Join(c.subgraphErrors, errors.Join(errs...))
func (c *Context) appendSubgraphErrors(ds DataSourceInfo, errs ...error) {
c.subgraphErrors[ds.Name] = errors.Join(c.subgraphErrors[ds.Name], errors.Join(errs...))
}
Comment thread
SkArchon marked this conversation as resolved.

type Request struct {
Expand All @@ -155,7 +172,8 @@ func NewContext(ctx context.Context) *Context {
panic("nil context.Context")
}
return &Context{
ctx: ctx,
ctx: ctx,
subgraphErrors: make(map[string]error),
}
}

Expand Down Expand Up @@ -190,6 +208,13 @@ func (c *Context) clone(ctx context.Context) *Context {
}
}

if c.subgraphErrors != nil {
cpy.subgraphErrors = make(map[string]error, len(c.subgraphErrors))
for k, v := range c.subgraphErrors {
cpy.subgraphErrors[k] = v
}
}

return &cpy
}

Expand Down
28 changes: 23 additions & 5 deletions v2/pkg/engine/resolve/extensions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ func TestExtensions(t *testing.T) {

res := generateTestFederationGraphQLResponse(t, ctrl)

return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer},
resolveCtx := NewContext(context.Background())
resolveCtx.authorizer = authorizer
return res, resolveCtx,
`{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null}}`,
func(t *testing.T) {}
}))
Expand All @@ -44,7 +46,11 @@ func TestExtensions(t *testing.T) {

res := generateTestFederationGraphQLResponse(t, ctrl)

return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}},
resolveCtx := NewContext(context.Background())
resolveCtx.authorizer = authorizer
resolveCtx.rateLimiter = limiter
resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}
return res, resolveCtx,
`{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"authorization":{"missingScopes":[["read:users"]]},"rateLimit":{"Policy":"policy","Allowed":0,"Used":0}}}`,
func(t *testing.T) {}
}))
Expand All @@ -69,7 +75,11 @@ func TestExtensions(t *testing.T) {

res := generateTestFederationGraphQLResponse(t, ctrl)

return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}},
resolveCtx := NewContext(context.Background())
resolveCtx.authorizer = authorizer
resolveCtx.rateLimiter = limiter
resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}
return res, resolveCtx,
`{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test.","extensions":{"code":"UNAUTHORIZED_FIELD_OR_TYPE"}},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"authorization":{"missingScopes":[["read:users"]]},"rateLimit":{"Policy":"policy","Allowed":0,"Used":0}}}`,
func(t *testing.T) {}
}))
Expand All @@ -91,7 +101,11 @@ func TestExtensions(t *testing.T) {

res := generateTestFederationGraphQLResponse(t, ctrl)

return res, &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}},
resolveCtx := NewContext(context.Background())
resolveCtx.authorizer = authorizer
resolveCtx.rateLimiter = limiter
resolveCtx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}
return res, resolveCtx,
`{"errors":[{"message":"Rate limit exceeded for Subgraph 'users' at Path 'query', Reason: rate limit exceeded."},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"Failed to render Fetch Input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"rateLimit":{"Policy":"policy","Allowed":0,"Used":1}}}`,
func(t *testing.T) {}
}))
Expand All @@ -116,7 +130,11 @@ func TestExtensions(t *testing.T) {

res := generateTestFederationGraphQLResponse(t, ctrl)

ctx = &Context{ctx: context.Background(), Variables: nil, authorizer: authorizer, rateLimiter: limiter, RateLimitOptions: RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}, TracingOptions: TraceOptions{Enable: true, IncludeTraceOutputInResponseExtensions: true, EnablePredictableDebugTimings: true, Debug: true}}
ctx = NewContext(context.Background())
ctx.authorizer = authorizer
ctx.rateLimiter = limiter
ctx.RateLimitOptions = RateLimitOptions{Enable: true, IncludeStatsInResponseExtension: true}
ctx.TracingOptions = TraceOptions{Enable: true, IncludeTraceOutputInResponseExtensions: true, EnablePredictableDebugTimings: true, Debug: true}
ctx.ctx = SetTraceStart(ctx.ctx, true)

return res, ctx,
Expand Down
14 changes: 7 additions & 7 deletions v2/pkg/engine/resolve/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ func (ri *ResponseInfo) GetResponseBody() string {
return ri.responseBody.String()
}

func newResponseInfo(res *result, subgraphError error) *ResponseInfo {
func newResponseInfo(res *result, subgraphErrors map[string]error) *ResponseInfo {
responseInfo := &ResponseInfo{
StatusCode: res.statusCode,
Err: subgraphError,
Err: subgraphErrors[res.ds.Name],
responseBody: res.out,
}
if res.httpResponseContext != nil {
Expand Down Expand Up @@ -740,7 +740,7 @@ func (l *Loader) appendSubgraphError(res *result, fetchItem *FetchItem, value *a
subgraphError.AppendDownstreamError(&gErr)
}

l.ctx.appendSubgraphErrors(res.err, subgraphError)
l.ctx.appendSubgraphErrors(res.ds, res.err, subgraphError)

return nil
}
Expand Down Expand Up @@ -1088,7 +1088,7 @@ func (l *Loader) renderErrorsFailedDeps(fetchItem *FetchItem, res *result) error
}

func (l *Loader) renderErrorsFailedToFetch(fetchItem *FetchItem, res *result, reason string) error {
l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
errorObject, err := astjson.ParseWithoutCache(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, reason))
if err != nil {
return err
Expand All @@ -1104,7 +1104,7 @@ func (l *Loader) renderErrorsStatusFallback(fetchItem *FetchItem, res *result, s
reason += fmt.Sprintf(": %s", statusText)
}

l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))

errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"%s"}`, reason))
if err != nil {
Expand Down Expand Up @@ -1133,7 +1133,7 @@ func (l *Loader) renderSubgraphBaseError(ds DataSourceInfo, path, reason string)

func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *result) error {
for i := range res.authorizationRejectedReasons {
l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode))
l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode))
}
pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath)
extensionErrorCode := fmt.Sprintf(`"extensions":{"code":"%s"}`, errorcodes.UnauthorizedFieldOrType)
Expand Down Expand Up @@ -1174,7 +1174,7 @@ func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *re
}

func (l *Loader) renderRateLimitRejectedErrors(fetchItem *FetchItem, res *result) error {
l.ctx.appendSubgraphErrors(res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason))
l.ctx.appendSubgraphErrors(res.ds, res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason))
pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath)
var (
err error
Expand Down
Loading