diff --git a/graphql/e2e/custom_logic/cmd/main.go b/graphql/e2e/custom_logic/cmd/main.go index 480ed15357d..5fe11f37caa 100644 --- a/graphql/e2e/custom_logic/cmd/main.go +++ b/graphql/e2e/custom_logic/cmd/main.go @@ -249,6 +249,26 @@ func getDefaultResponse() []byte { return []byte(resTemplate) } +func getRestError(w http.ResponseWriter, err []byte) { + w.WriteHeader(http.StatusBadRequest) + check2(w.Write(err)) +} + +func getFavMoviesErrorHandler(w http.ResponseWriter, r *http.Request) { + err := verifyRequest(r, expectedRequest{ + method: http.MethodGet, + urlSuffix: "/0x123?name=Author&num=10", + body: "", + headers: nil, + }) + if err != nil { + check2(w.Write([]byte(err.Error()))) + return + } + + getRestError(w, []byte(`{"errors":[{"message": "Rest API returns Error for myFavoriteMovies query","locations": [ { "line": 5, "column": 4 } ],"path": ["Movies","name"]}]}`)) +} + func getFavMoviesHandler(w http.ResponseWriter, r *http.Request) { err := verifyRequest(r, expectedRequest{ method: http.MethodGet, @@ -398,6 +418,20 @@ func favMoviesCreateHandler(w http.ResponseWriter, r *http.Request) { ]`))) } +func favMoviesCreateErrorHandler(w http.ResponseWriter, r *http.Request) { + err := verifyRequest(r, expectedRequest{ + method: http.MethodPost, + urlSuffix: "/favMoviesCreateError", + body: `{"movies":[{"director":[{"name":"Dir1"}],"name":"Mov1"},{"name":"Mov2"}]}`, + headers: nil, + }) + if err != nil { + check2(w.Write([]byte(err.Error()))) + return + } + getRestError(w, []byte(`{"errors":[{"message": "Rest API returns Error for FavoriteMoviesCreate query"}]}`)) +} + func favMoviesCreateWithNullBodyHandler(w http.ResponseWriter, r *http.Request) { err := verifyRequest(r, expectedRequest{ method: http.MethodPost, @@ -836,6 +870,10 @@ func userNameHandler(w http.ResponseWriter, r *http.Request) { nameHandler(w, r, &inputBody) } +func userNameErrorHandler(w http.ResponseWriter, r *http.Request) { + getRestError(w, []byte(`{"errors":[{"message": "Rest API returns Error for field name"}]}`)) +} + func userNameWithoutAddressHandler(w http.ResponseWriter, r *http.Request) { expectedRequest := expectedRequest{ body: `{"uid":"0x5"}`, @@ -1210,6 +1248,7 @@ func main() { // for queries http.HandleFunc("/favMovies/", getFavMoviesHandler) + http.HandleFunc("/favMoviesError/", getFavMoviesErrorHandler) http.HandleFunc("/favMoviesPost/", postFavMoviesHandler) http.HandleFunc("/favMoviesPostWithBody/", postFavMoviesWithBodyHandler) http.HandleFunc("/verifyHeaders", verifyHeadersHandler) @@ -1218,6 +1257,7 @@ func main() { // for mutations http.HandleFunc("/favMoviesCreate", favMoviesCreateHandler) + http.HandleFunc("/favMoviesCreateError", favMoviesCreateErrorHandler) http.HandleFunc("/favMoviesUpdate/", favMoviesUpdateHandler) http.HandleFunc("/favMoviesDelete/", favMoviesDeleteHandler) http.HandleFunc("/favMoviesCreateWithNullBody", favMoviesCreateWithNullBodyHandler) @@ -1232,6 +1272,7 @@ func main() { // for testing single mode http.HandleFunc("/userName", userNameHandler) + http.HandleFunc("/userNameError", userNameErrorHandler) http.HandleFunc("/userNameWithoutAddress", userNameWithoutAddressHandler) http.HandleFunc("/checkHeadersForUserName", userNameHandlerWithHeaders) http.HandleFunc("/car", carHandler) diff --git a/graphql/e2e/custom_logic/custom_logic_test.go b/graphql/e2e/custom_logic/custom_logic_test.go index 563ddb9d373..d5139dc5788 100644 --- a/graphql/e2e/custom_logic/custom_logic_test.go +++ b/graphql/e2e/custom_logic/custom_logic_test.go @@ -472,7 +472,7 @@ func TestCustomQueryWithNonExistentURLShouldReturnError(t *testing.T) { require.Equal(t, x.GqlErrorList{ { Message: "Evaluation of custom field failed because external request returned an " + - "error: unexpected status code: 404 for field: myFavoriteMovies within" + + "error: unexpected error with: 404 for field: myFavoriteMovies within" + " type: Query.", Locations: []x.Location{{Line: 3, Column: 3}}, }, @@ -557,10 +557,10 @@ func TestCustomQueryShouldPropagateErrorFromFields(t *testing.T) { expectedErrors := x.GqlErrorList{ &x.GqlError{Message: "Evaluation of custom field failed because external request " + - "returned an error: unexpected status code: 404 for field: cars within type: Person.", + "returned an error: unexpected error with: 404 for field: cars within type: Person.", Locations: []x.Location{{Line: 6, Column: 4}}}, &x.GqlError{Message: "Evaluation of custom field failed because external request returned" + - " an error: unexpected status code: 404 for field: bikes within type: Person.", + " an error: unexpected error with: 404 for field: bikes within type: Person.", Locations: []x.Location{{Line: 9, Column: 4}}}, } require.Contains(t, result.Errors, expectedErrors[0]) @@ -2808,3 +2808,184 @@ func TestCustomDQL(t *testing.T) { ] }`, string(result.Data)) } + +func TestCustomGetQuerywithRESTError(t *testing.T) { + schema := customTypes + ` + type Query { + myFavoriteMovies(id: ID!, name: String!, num: Int): [Movie] @custom(http: { + url: "http://mock:8888/favMoviesError/$id?name=$name&num=$num", + method: "GET" + }) + }` + updateSchemaRequireNoGQLErrors(t, schema) + time.Sleep(2 * time.Second) + + query := ` + query { + myFavoriteMovies(id: "0x123", name: "Author", num: 10) { + id + name + director { + id + name + } + } + }` + params := &common.GraphQLParams{ + Query: query, + } + + result := params.ExecuteAsPost(t, alphaURL) + require.Equal(t, x.GqlErrorList{ + { + Message: "Rest API returns Error for myFavoriteMovies query", + Locations: []x.Location{{Line: 5, Column: 4}}, + Path: []interface{}{"Movies", "name"}, + }, + }, result.Errors) + +} + +func TestCustomFieldsWithRestError(t *testing.T) { + schema := ` + type Car @remote { + id: ID! + name: String! + } + + type User { + id: String! @id @search(by: [hash, regexp]) + name: String + @custom( + http: { + url: "http://mock:8888//userNameError" + method: "GET" + body: "{uid: $id}" + mode: SINGLE, + } + ) + age: Int! @search + cars: Car + @custom( + http: { + url: "http://mock:8888/cars" + method: "GET" + body: "{uid: $id}" + mode: BATCH, + } + ) + } + ` + + updateSchemaRequireNoGQLErrors(t, schema) + time.Sleep(2 * time.Second) + + params := &common.GraphQLParams{ + Query: `mutation addUser { + addUser(input: [{ id:"0x1", age: 10 }]) { + user { + id + age + } + } + }`, + } + + result := params.ExecuteAsPost(t, alphaURL) + common.RequireNoGQLErrors(t, result) + + queryUser := ` + query ($id: String!){ + queryUser(filter: {id: {eq: $id}}) { + id + name + age + cars{ + name + } + } + }` + + params = &common.GraphQLParams{ + Query: queryUser, + Variables: map[string]interface{}{"id": "0x1"}, + } + + result = params.ExecuteAsPost(t, alphaURL) + + expected := ` + { + "queryUser": [ + { + "id": "0x1", + "name": null, + "age": 10, + "cars": { + "name": "car-0x1" + } + } + ] + }` + + require.Equal(t, x.GqlErrorList{ + { + Message: "Rest API returns Error for field name", + }, + }, result.Errors) + + require.JSONEq(t, expected, string(result.Data)) + +} + +func TestCustomPostMutationWithRESTError(t *testing.T) { + schema := customTypes + ` + input MovieDirectorInput { + id: ID + name: String + directed: [MovieInput] + } + input MovieInput { + id: ID + name: String + director: [MovieDirectorInput] + } + type Mutation { + createMyFavouriteMovies(input: [MovieInput!]): [Movie] @custom(http: { + url: "http://mock:8888/favMoviesCreateError", + method: "POST", + body: "{ movies: $input}" + }) + }` + updateSchemaRequireNoGQLErrors(t, schema) + time.Sleep(2 * time.Second) + + params := &common.GraphQLParams{ + Query: ` + mutation createMovies($movs: [MovieInput!]) { + createMyFavouriteMovies(input: $movs) { + id + name + director { + id + name + } + } + }`, + Variables: map[string]interface{}{ + "movs": []interface{}{ + map[string]interface{}{ + "name": "Mov1", + "director": []interface{}{map[string]interface{}{"name": "Dir1"}}, + }, + map[string]interface{}{"name": "Mov2"}, + }}, + } + + result := params.ExecuteAsPost(t, alphaURL) + require.Equal(t, x.GqlErrorList{ + { + Message: "Rest API returns Error for FavoriteMoviesCreate query", + }, + }, result.Errors) + +} diff --git a/graphql/resolve/resolver.go b/graphql/resolve/resolver.go index b04c4bb6e1e..06d2fada325 100644 --- a/graphql/resolve/resolver.go +++ b/graphql/resolve/resolver.go @@ -174,6 +174,11 @@ type Resolved struct { Extensions *schema.Extensions } +// restErr is Error returned from custom REST endpoint +type restErr struct { + Errors x.GqlErrorList `json:"errors,omitempty"` +} + // CompletionFunc is an adapter that allows us to compose completions and build a // ResultCompleter from a function. Based on the http.HandlerFunc pattern. type CompletionFunc func(ctx context.Context, resolved *Resolved) @@ -880,7 +885,7 @@ func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, er return } - b, err = makeRequest(nil, fconf.Method, fconf.URL, string(b), fconf.ForwardHeaders) + b, status, err := makeRequest(nil, fconf.Method, fconf.URL, string(b), fconf.ForwardHeaders) if err != nil { errCh <- x.GqlErrorList{externalRequestError(err, f)} return @@ -889,6 +894,7 @@ func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, er // To collect errors from remote GraphQL endpoint and those encountered during execution. var errs error var result []interface{} + var rerr restErr if graphql { resp := &graphqlResp{} err = json.Unmarshal(b, resp) @@ -906,11 +912,23 @@ func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, er errCh <- schema.AppendGQLErrs(errs, keyNotFoundError(f, fconf.RemoteGqlQueryName)) return } - } else if err := json.Unmarshal(b, &result); err != nil { - errCh <- x.GqlErrorList{jsonUnmarshalError(err, f)} - return + } else { + if status >= 200 && status < 300 { + if err = json.Unmarshal(b, &result); err != nil { + errCh <- x.GqlErrorList{jsonUnmarshalError(err, f)} + return + } + } else { + if err = json.Unmarshal(b, &rerr); err != nil { + err = errors.Errorf("unexpected error with: %v", status) + errCh <- x.GqlErrorList{externalRequestError(err, f)} + return + } else { + errCh <- rerr.Errors + return + } + } } - if len(result) != len(vals) { gqlErr := x.GqlErrorf("Evaluation of custom field failed because expected result of "+ "external request to be of size %v, got: %v for field: %s within type: %s.", @@ -972,18 +990,18 @@ func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, er mu.RUnlock() } - b, err = makeRequest(nil, fconf.Method, url, string(b), fconf.ForwardHeaders) + b, status, err := makeRequest(nil, fconf.Method, url, string(b), fconf.ForwardHeaders) if err != nil { errChan <- x.GqlErrorList{externalRequestError(err, f)} return } var result interface{} + var rerr restErr var errs error if graphql { resp := &graphqlResp{} - err = json.Unmarshal(b, resp) - if err != nil { + if err = json.Unmarshal(b, resp); err != nil { errChan <- x.GqlErrorList{jsonUnmarshalError(err, f)} return } @@ -998,11 +1016,23 @@ func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, er keyNotFoundError(f, fconf.RemoteGqlQueryName)) return } - } else if err := json.Unmarshal(b, &result); err != nil { - errChan <- x.GqlErrorList{jsonUnmarshalError(err, f)} - return + } else { + if status >= 200 && status < 300 { + if err = json.Unmarshal(b, &result); err != nil { + errCh <- x.GqlErrorList{jsonUnmarshalError(err, f)} + return + } + } else { + if err = json.Unmarshal(b, &rerr); err != nil { + err = errors.Errorf("unexpected error with: %v", status) + errCh <- x.GqlErrorList{externalRequestError(err, f)} + return + } else { + errCh <- rerr.Errors + return + } + } } - mu.Lock() val, ok := vals[idx].(map[string]interface{}) if ok { @@ -1783,7 +1813,7 @@ func (hr *httpResolver) Resolve(ctx context.Context, field schema.Field) *Resolv } func makeRequest(client *http.Client, method, url, body string, - header http.Header) ([]byte, error) { + header http.Header) ([]byte, int, error) { var reqBody io.Reader if body == "" || body == "null" { reqBody = http.NoBody @@ -1793,7 +1823,7 @@ func makeRequest(client *http.Client, method, url, body string, req, err := http.NewRequest(method, url, reqBody) if err != nil { - return nil, err + return nil, 0, err } req.Header = header @@ -1805,16 +1835,13 @@ func makeRequest(client *http.Client, method, url, body string, } resp, err := client.Do(req) if err != nil { - return nil, err - } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return nil, errors.Errorf("unexpected status code: %v", resp.StatusCode) + return nil, 0, err } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) - return b, err + return b, resp.StatusCode, err } func (hr *httpResolver) rewriteAndExecute(ctx context.Context, field schema.Field) *Resolved { @@ -1840,7 +1867,7 @@ func (hr *httpResolver) rewriteAndExecute(ctx context.Context, field schema.Fiel body = string(b) } - b, err := makeRequest(hr.Client, hrc.Method, hrc.URL, body, hrc.ForwardHeaders) + b, status, err := makeRequest(hr.Client, hrc.Method, hrc.URL, body, hrc.ForwardHeaders) if err != nil { return emptyResult(externalRequestError(err, field)) } @@ -1848,12 +1875,20 @@ func (hr *httpResolver) rewriteAndExecute(ctx context.Context, field schema.Fiel // this means it had body and not graphql, so just unmarshal it and return if hrc.RemoteGqlQueryName == "" { var result interface{} - if err := json.Unmarshal(b, &result); err != nil { - return emptyResult(jsonUnmarshalError(err, field)) + var rerr restErr + if status >= 200 && status < 300 { + if err := json.Unmarshal(b, &result); err != nil { + return emptyResult(jsonUnmarshalError(err, field)) + } + } else if err := json.Unmarshal(b, &rerr); err != nil { + err = errors.Errorf("unexpected error with: %v", status) + rerr.Errors = x.GqlErrorList{externalRequestError(err, field)} } + return &Resolved{ Data: map[string]interface{}{field.Name(): result}, Field: field, + Err: rerr.Errors, } } @@ -1872,7 +1907,6 @@ func (hr *httpResolver) rewriteAndExecute(ctx context.Context, field schema.Fiel if !ok { return emptyResult(resp.Errors) } - return &Resolved{ Data: map[string]interface{}{field.Name(): data}, Field: field,