Is your feature request related to a problem? Please describe.
I'm experimenting with using genqlient for querying the GitHub v4 API.
Things work for the most part, except one problem:
GitHub's error objects don't match errors in the GraphQL spec exactly.
Problem
Per the GraphQL spec spec,
error objects include message, locations, and path.
An "extensions" field may contain any other arbitrary information that the
server wants to communicate, e.g. programmatically consumable error codes.
Example
{
"errors": [
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [{ "line": 6, "column": 7 }],
"path": ["hero", "heroFriends", 1, "name"],
"extensions": {
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
}
}
]
}
GitHub's GraphQL API puts the error code in a "type" field of the error objects.
Example:
{
"data": {
"repository": null
},
"errors": [
{
"type": "NOT_FOUND",
"path": ["repository"],
"locations": [{"line": 1, "column": 36}],
"message": "Could not resolve to a Repository with the name ''abhinav/does-not-exist-repo''."
}
]
}
The error is returned in a separate "type" field.
This field is not present on the
underlying gqlerror.List type
so it's never parsed and cannot be accessed.
Reproduction
-
Download GitHub GraphQL schema.
curl -L https://docs.github.com/public/fpt/schema.docs.graphql > github.graphql
-
Write a query.
# query.graphql
query lookupRepositoryID(
$owner: String!
$name: String!
) {
repository(owner: $owner, name: $name) {
id
}
}
-
Configure code generation.
# genqlient.yaml
schema: github.graphql
operations:
- "query.graphql"
generated: query_generated.go
package: github
-
Generate code.
go tool github.com/Khan/genqlient
-
Use generated code
// query_test.go
package github
import (
"fmt"
"os"
"testing"
"github.com/Khan/genqlient/graphql"
"github.com/stretchr/testify/require"
"github.com/vektah/gqlparser/v2/gqlerror"
"golang.org/x/oauth2"
)
func TestLookupRepositoryID(t *testing.T) {
token := os.Getenv("GITHUB_TOKEN")
require.NotEmpty(t, token, "token must not be empty")
httpClient := oauth2.NewClient(t.Context(),
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}),
)
gqlClient := graphql.NewClient("https://api.github.com/graphql", httpClient)
_, err := lookupRepositoryID(t.Context(), gqlClient, "abhinav", "does-not-exist")
require.Error(t, err)
var gqlErr gqlerror.List
require.ErrorAs(t, err, &gqlErr)
fmt.Printf("%#v\n", gqlErr[0])
// Cannot access "type" of the error.
}
The call fails as expected by the test
but there's no way to access the underlying error "type".
That error codes are communicated like this can also be verified in the
GraphQL client used by gh CLI extensions
(a fork of shurcooL/graphql).
I recognize that this is a weirdness of GitHub's implementation of GraphQL,
but strictly speaking, it is permitted by the spec:
GraphQL services should not provide any additional entries to the error
format since they could conflict with additional entries that may be added in
future versions of this specification.
Note: Previous versions of this spec did not describe the extensions entry
for error formatting.
While non-specified entries are not violations, they are still discouraged.
(Emphasis mine.)
So I'm wondering whether there's a way for genqlient to support this.
Describe the solution you'd like
Currently, genqlient uses gqlerror.List
from vektah/gqlparser for the error response.
Any solution here will likely require a custom error type (exported or otherwise).
I'll cover those below but first, let's talk about backwards compatibility:
the custom error type will want to implement As(any) bool and Is(any) bool
to control the behavior of errors.Is and errors.As.
The implementations will convert the error to gqlerror.List or gqlerror.Error,
or compare against them as needed, so that existing code that consumes gqlerror
does not break.
A couple possible solutions:
-
First-class support for "type"
genqlient will export a type Error and type ErrorList.
Error will have a Type string field.
type Error struct {
Message string
Path ast.Path
Locations []Location
+ Type string `json:"type"`
Pros:
Cons:
- GitHub-specific solution.
Won't generalize to other similarly discouraged (but spec-compliant)
server implementations.
-
Untyped error data
genqlient will export a type Error and type ErrorList.
Error will have the standard Message, Path, Locations, and Extensions field,
and a new Data map[string]any field that contains any JSON field received
that are not any of the others.
type Error struct {
Message string
Path ast.Path
Locations []Location
+ Data map[string]any
(This will obviously need custom MarshalJSON and UnmarshalJSON methods
that handle handling the JSON bag of values correctly.)
GitHub v4 consumers will do something like:
var (
gqlErrs graphql.ErrorList
errs []error
)
if errors.As(err, &gqlErrs) {
for _, gqlErr := range gqlErrs {
typ, ok := gqlErr.Data["type"].(string)
if ok && typ == "NOT_FOUND" {
errs = append(errs, ErrNotFound)
}
}
}
Pros:
- General solution for GitHub and non-GitHub cases.
Will handle any such spec-compliant non-standard error responses.
Cons:
- Not type safe.
- Fairly complex Error type is now part of genqlient's public API.
It's likely to leak into downstream code, and is easy to break.
-
User-customizable error shape
genqlient will support a new configuration option
to change the error list type in use.
This will default to gqlerror.List,
but the user will be able to specify their own.
# error_list_type is the type used for the list of errors
# returned in a GraphQL response.
#
# The type specified here MUST implement the error interface.
error_list_type: "github.com/example/foo.githubv4ErrorList"
type githubv4ErrorList []struct {
Message string
Path []any
Locations []struct{ Line, Column int }
Type string
Extensions map[string]any
}
Implementation-wise, this can probably be done with a type-parameterized
Response type, e.g.
type GResponse[Data any, Errors error] struct { // suggestions for better names?
Data Data
Extensions map[string]any
Errors Errors
}
type Response[T any] = GResponse[T, gqlerror.List] // needs Go >= 1.23
(I haven't fully thought this through; this may require separate
error_type and error_list_type configuration options.
Or we might be able to use only an error_type + an ErrorList[E]
situation.)
Pros:
- General solution for GitHub and non-GitHub cases.
Will handle any such spec-compliant non-standard error responses.
- Can be as type-safe or type-unsafe as the user prefers.
Cons:
- Users must add Message, Path, Locations, and Extensions themselves.
- Requires raising minimum required Go version to 1.23 to use generic type alias.
Describe alternatives you've considered
While I lean towards solution (3), I'd love to hear from maintainers
whether this is something worth solving in genqlient,
and whether there is a better design approach here.
One workaround I've considered (and used with shurcooL/githubv4)
is to intercept the response payload before the GraphQL client has a go at it,
and parse the error codes out there myself.
Implementation here.
If y'all decide that this isn't something worth supporting in genqlient,
that's probably the route I'll try to take but it smells a bit to parse the payload twice.
Is your feature request related to a problem? Please describe.
I'm experimenting with using genqlient for querying the GitHub v4 API.
Things work for the most part, except one problem:
GitHub's error objects don't match errors in the GraphQL spec exactly.
Problem
Per the GraphQL spec spec,
error objects include message, locations, and path.
An "extensions" field may contain any other arbitrary information that the
server wants to communicate, e.g. programmatically consumable error codes.
Example
{ "errors": [ { "message": "Name for character with ID 1002 could not be fetched.", "locations": [{ "line": 6, "column": 7 }], "path": ["hero", "heroFriends", 1, "name"], "extensions": { "code": "CAN_NOT_FETCH_BY_ID", "timestamp": "Fri Feb 9 14:33:09 UTC 2018" } } ] }GitHub's GraphQL API puts the error code in a "type" field of the error objects.
Example:
{ "data": { "repository": null }, "errors": [ { "type": "NOT_FOUND", "path": ["repository"], "locations": [{"line": 1, "column": 36}], "message": "Could not resolve to a Repository with the name ''abhinav/does-not-exist-repo''." } ] }The error is returned in a separate "type" field.
This field is not present on the
underlying gqlerror.List type
so it's never parsed and cannot be accessed.
Reproduction
Download GitHub GraphQL schema.
curl -L https://docs.github.com/public/fpt/schema.docs.graphql > github.graphqlWrite a query.
Configure code generation.
Generate code.
Use generated code
The call fails as expected by the test
but there's no way to access the underlying error "type".
That error codes are communicated like this can also be verified in the
GraphQL client used by gh CLI extensions
(a fork of shurcooL/graphql).
I recognize that this is a weirdness of GitHub's implementation of GraphQL,
but strictly speaking, it is permitted by the spec:
(Emphasis mine.)
So I'm wondering whether there's a way for genqlient to support this.
Describe the solution you'd like
Currently, genqlient uses gqlerror.List
from vektah/gqlparser for the error response.
Any solution here will likely require a custom error type (exported or otherwise).
I'll cover those below but first, let's talk about backwards compatibility:
the custom error type will want to implement
As(any) boolandIs(any) boolto control the behavior of
errors.Isanderrors.As.The implementations will convert the error to
gqlerror.Listorgqlerror.Error,or compare against them as needed, so that existing code that consumes gqlerror
does not break.
A couple possible solutions:
First-class support for "type"
genqlient will export a
type Errorandtype ErrorList.Errorwill have aType stringfield.type Error struct { Message string Path ast.Path Locations []Location + Type string `json:"type"`Pros:
Cons:
Won't generalize to other similarly discouraged (but spec-compliant)
server implementations.
Untyped error data
genqlient will export a
type Errorandtype ErrorList.Errorwill have the standard Message, Path, Locations, and Extensions field,and a new
Data map[string]anyfield that contains any JSON field receivedthat are not any of the others.
type Error struct { Message string Path ast.Path Locations []Location + Data map[string]any(This will obviously need custom MarshalJSON and UnmarshalJSON methods
that handle handling the JSON bag of values correctly.)
GitHub v4 consumers will do something like:
Pros:
Will handle any such spec-compliant non-standard error responses.
Cons:
It's likely to leak into downstream code, and is easy to break.
User-customizable error shape
genqlient will support a new configuration option
to change the error list type in use.
This will default to gqlerror.List,
but the user will be able to specify their own.
Implementation-wise, this can probably be done with a type-parameterized
Response type, e.g.
(I haven't fully thought this through; this may require separate
error_typeanderror_list_typeconfiguration options.Or we might be able to use only an
error_type+ anErrorList[E]situation.)
Pros:
Will handle any such spec-compliant non-standard error responses.
Cons:
Describe alternatives you've considered
While I lean towards solution (3), I'd love to hear from maintainers
whether this is something worth solving in genqlient,
and whether there is a better design approach here.
One workaround I've considered (and used with shurcooL/githubv4)
is to intercept the response payload before the GraphQL client has a go at it,
and parse the error codes out there myself.
Implementation here.
If y'all decide that this isn't something worth supporting in genqlient,
that's probably the route I'll try to take but it smells a bit to parse the payload twice.