-
Notifications
You must be signed in to change notification settings - Fork 575
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
Integrate graphql-http, refactor and comply with spec (v3) #1589
Conversation
# Conflicts: # packages/graphql-yoga/__tests__/node.spec.ts # packages/graphql-yoga/src/error.ts # packages/graphql-yoga/src/plugins/requestParser/POSTJson.ts # packages/graphql-yoga/src/server.ts
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is per HTTP specification not GraphQL over HTTP, because this has nothing to do with GraphQL over HTTP since this validation is about the contract between client and server.
Sorry I've put this comment in a wrong place. I can write here something else :D
Why do we stop supporting multipart defer/stream and event stream responses. I think we are missing a lot of points of using plugins(modularized structure) and hooks.
@ardatan I don't follow, which validation? |
@@ -166,7 +166,11 @@ describe('accept header', () => { | |||
}, | |||
}, | |||
) | |||
expect(response.status).toEqual(406) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is per HTTP specification not GraphQL over HTTP, because this has nothing to do with GraphQL over HTTP since this validation is about the contract between client and server.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though, GraphQL over HTTP says we should return 406
in this case as I understand;
https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#body
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because when accepting application/json
the server should not use 4xx and 5xx (see spec).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But we don't accept application/json
, do we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do accept it, or do you mean we don't accept it in this situation? The server is set up now to respect the accept headers, if the client requests application/json
and makes an invalid operation - the server will error out respecting the accept requirements.
expect(response.status).toEqual(200) | ||
const body = await response.json() | ||
expect(body.errors).toEqual([ | ||
{ message: 'Subscriptions are not supported' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Message is not clear for me. It is not supported because Accept
doesn't conform the response's content type. 406
explains this better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, when accepting application/json
the server should not use 4xx and 5xx (see spec).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with above, we don't accept application/json
in this case, so we should return Not Acceptable
to the client. Also I think error message is too generic. It is hard to understand that the problem is the accepted mime type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#1589 (comment). I know that as per the HTTP spec 406 is obvious, but I am trying to follow the GQL over HTTP spec... I can improve the error message, it is indeed vague.
@@ -202,6 +206,10 @@ describe('accept header', () => { | |||
accept: 'application/json', | |||
}, | |||
}) | |||
expect(response.status).toEqual(406) | |||
expect(response.status).toEqual(200) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, when accepting application/json
the server should not use 4xx and 5xx (see spec).
@@ -145,12 +145,12 @@ describe('error masking', () => { | |||
} | |||
}) | |||
|
|||
it('non GraphQLError raised in onRequestParse is masked with the correct status code 500', async () => { | |||
it('non GraphQLError raised in onPrepare is masked with the correct status code 500', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onPrepare looks too generic. onRequestParse is getting triggered after onRequest
(request received) and when GraphQLParams
is requested if any other plugin ended the response earlier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onPrepare
does the "preparation" part, manipulates the params and accepts early results (for caching).
Name seemed like a right fit IMHO; but debatable, I agree.
@@ -186,15 +189,14 @@ describe('error masking', () => { | |||
}) | |||
const body = JSON.parse(await response.text()) | |||
expect(body).toMatchInlineSnapshot(` | |||
Object { | |||
"data": null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should always have data
even if it is null
;
https://spec.graphql.org/draft/#sec-Response-Format
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the request failed before execution, due to a syntax error, missing information, or validation error, this entry must not be present.
Also I found this one, which I think we should cover as well.
@@ -287,7 +286,7 @@ describe('error masking', () => { | |||
}, | |||
], | |||
}) | |||
expect(response.status).toEqual(200) | |||
expect(response.status).toEqual(400) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why 400? 4xx(Bad Request) should be thrown by the client's mistake, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a change to default to 200/400 when throwing instances of GraphQLError
, and 200/500 when throwing instances of Error
. The handler will still respect the http extensions (if not from resolvers).
I still think a generic "plain" GraphQL over http server can already be created by using the parts deleted here with a few lines code. import { DocumentNode, execute, ExecutionArgs, GraphQLSchema, parse, validate } from "graphql";
import { assertMutationViaGet, assertValidGraphQLParams, GraphQLParams, isGETRequest, isPOSTGraphQLStringRequest, isPOSTJsonRequest, isRegularResult, parseGETRequest, parsePOSTGraphQLStringRequest, parsePOSTJsonRequest } from 'graphql-yoga'; // Or any other core package we would introduce in this case
export async function handleGraphQLoverHTTPRequest(
request: Request,
schema: GraphQLSchema,
): Promise<Response> {
let params: GraphQLParams | undefined;
if (isGETRequest(request)) {
params = parseGETRequest(request);
}
if (isPOSTJsonRequest(request)) {
params = await parsePOSTJsonRequest(request);
}
if (isPOSTGraphQLStringRequest(request)) {
params = await parsePOSTGraphQLStringRequest(request);
}
try {
assertValidGraphQLParams(params);
} catch (e) {
return new Response(JSON.stringify(e), {
status: e.extensions.http.status,
headers: e.extensions.http.headers,
})
}
let document: DocumentNode;
try {
document = parse(params.query!);
} catch (e) {
return new Response(JSON.stringify(e), {
status: 400,
})
}
try {
assertMutationViaGet(request.method, document, params.operationName);
} catch (e) {
return new Response(JSON.stringify(e), {
status: e.extensions.http.status,
headers: e.extensions.http.headers,
})
}
const errors = validate(schema, document);
if (errors?.length > 0) {
return new Response(JSON.stringify({ errors }), {
status: 400,
headers: {
'Content-Type': 'application/json',
}
});
}
const executionArgs: ExecutionArgs = {
schema,
document,
contextValue: {
request,
},
variableValues: params.variables,
operationName: params.operationName,
};
const result = await execute(executionArgs);
if (isRegularResult(request, result)) {
return new Response(JSON.stringify(result), {
headers: {
'Content-Type': 'application/json',
}
});
}
return new Response('Not implemented', {
status: 500,
})
} |
@@ -39,120 +32,4 @@ describe('GraphQLError.extensions.http', () => { | |||
expect(response.status).toBe(401) | |||
expect(response.headers.get('www-authenticate')).toBe('Bearer') | |||
}) | |||
|
|||
it('picks the highest status code and headers for GraphQLErrors thrown within multiple resolvers', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this because we want to give the user to control the response parameters. Also we benefit from this in our plugins in the execution level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing errors from field resolvers will nullify only that field, for example:
query { a }
and resolver for a
errors out, the response will be:
{ data: { a: null }, errors: [{ message: 'Woah!' }] }
As per the spec, if the data
field is not-null, the response code must be 200. (For accepting application/json and for accepting application/graphql-response+json)
@@ -57,6 +58,7 @@ describe('Automatic Persisted Queries', () => { | |||
const response = await request(yoga) | |||
.post('/graphql') | |||
.send({ | |||
query: '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think clients supporting APQ provide query
. If they don't, this would break the support of those clients.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but this is a different spec we follow. We are going beyond spec here and we want to support clients instead of introducing our own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to be spec compliant query must not only be present but also must be the string representation of the GraphQL document. An empty string does not satisfy the spec (and is weird/confusing).
Persisted operations are deliberately not covered by the GraphQL-over-HTTP spec right now, this is because the spec is to increase interoperability but persisted operations (when used as an allowlist) have exactly the opposite aim. That’s fine - you can use persisted operations alongside a GraphQL compliant server, it’s just an alternative communication method (not spec compliant, but still perfectly fine). We’ll probably incorporate some wording for persisted operations in a post-1.0 release of the spec.
) => PromiseOrValue<void> | ||
|
||
export interface OnRequestParseDoneEventPayload { | ||
export interface OnPrepareEventPayload { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onRequestParse and onRequestParseDone hooks are seperated because subsequent plugins can change the way of parsing before parsing has been done by Yoga's request parsers, so requestParser
isn't called in here yet.
onRequestParseDone
is called after the parameters are parsed in order to let the user change the parsed params for the execution. For example, persisted operations use this to manipulate the parameters for the following execution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that, my idea was to not have the user control the parsing. Yoga should implement all official and unofficial parsing techniques and do the parsing for the user, and have him manipulate the results only.
But this is debatable...
@ardatan not my intention to stop supporting them, bugs are failing the integration tests (I've been running
@ardatan I follow. I just am of the opinion that users won't need that kind of granularity, I think the benefit of Yoga is not to do any heavy lifting - just plug-n-play. Furthermore, your example is not spec compliant in terms of the accepted content-type dictating the status codes. It would be different depending on whether the server accepted |
Now with all tests passing (that are passing on v3 branch). |
# Conflicts: # packages/graphql-yoga/src/plugins/requestValidation/usePreventMutationViaGET.ts
Closing in favour of #1685. Might revisit sometimes in the future, but not soon. |
This PR encapsulates graphql-http integration, adjustments to comply with the spec and my take on graphql-yoga following my investigation and current understanding.
Please note that this PR is a proposal and would love to discuss this and adjust accordingly!
Key takeways:
application/graphql+json
for backwards compatibility? why?)application/json
response content-type should NOT use 4xx or 5xx status codes, always 200application/graphql-response+json
response content-type should use 4xx or 5xx on request errorsapplication/graphql-response+json
response content-type is recommended to use 200 status code if thedata
field is present (even if null, but defined)application/graphql-response+json
is a response content-type, requests should always useapplication/json
onResultProcess
plugin for now. I reckon it should be reintroduced as per @n1ru4l suggestion of giving the user the ability to alter the result before responding.onRequestParse
andonRequestParseDone
plugins and renamed it toonPrepare
(better name in my opinion since you're "preparing" stuff for execution, like the parameters or an early result).onRequest
andonRespond
plugins stay as is, they're very usefulparseX
andresultX
can be made public and reused to construct own request handlers with the plugins%future added value
for everything I might've forgot to mention 😅