-
Notifications
You must be signed in to change notification settings - Fork 48
Conversation
Fix CI More experiments Better errors ... ... More Fix status code Use babel instead of ts-jest Use ReadableStream polyfill Go for it .. Try out Avoid directory import No more ponyfills No more dependencies deno Update examples Update deno
The latest changes of this PR are available as canary in npm (based on the declared |
❌ Benchmark FailedFailed assertions detected: some GraphQL operations included in the loadtest are failing.
|
* Fastify support * Calculate string length * Fix fastify server * Update fastify example * Fix benchmarks * Try sth else * Finalize fastify support * Do not stringify JSON again
This pull request is being automatically deployed with Vercel (learn more). 🔍 Inspect: https://vercel.com/graphql-helix/graphql-helix/7wfJNjihBXfi8JPjVPeCvAFFneDa |
Deployment failed with the following error:
|
case "GET": { | ||
operationName = url.searchParams.get("operationName") || undefined; | ||
query = url.searchParams.get("query") || undefined; | ||
variables = url.searchParams.get("variables") || undefined; | ||
break; | ||
} |
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.
can the post and get avenues be split? Sometimes the GET routes one doesnt want 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.
Isn't that something user can block in their server impl?
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.
they can, but then there is 5 lines of code shipped doing absolutely nothing. Its simple;
getGETHandler();
getPOSTHandler();
gethandleReqRes(); // that calls both.
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.
Helix isnt about making decisions for you, its a "build your own server" library. Making the decision that GET/POST is supported isnt something that is in scope. All the user to "build their own server". Helix should just handle the query in and execution layers, leave the rest up to helpers.
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 separating get/post will break SSE for subscriptions, since it's using GET for the Event-Stream.
@n1ru4l please confirm?
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 generally would still expose that as 2 methods, as some folk won't be using sse. Export and consume what you're going to use.
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.
Actually SSE can be done with both POST and GET. I have been thinking for a while how we could make every functionality of helix more modular.
The HTTP verb specifies how the GraphQL payload sent by the client should be parsed. So on the server we could have some kind of pattern for parsing that:
const payload = await parsePayload({
POST: postParser,
GET: getParser
})(incomingRequest)
if people want custom behavior or non standard stuff they can simple replace the handler for a given method or remove it.
This approach is more verbose in boilerplate but a bit more modular in giving. A higher level API such as yoga can then provide a opinionated API.
The response protocol is then identified via the content-type and/or accept header, but also the operation type. Today the specification is pretty vague about this, I opened graphql/graphql-over-http#167 a while ago.
We can have a similar API to how we parse the payload for the response protocol:
const response = await matchResponse([
{ match: isSSE ,handler: sseResponseBuilder },
{ match: isMultiPart, handler: multiPartResponseBuilder },
{ match: isApplicationGraphQL, handler: applicationGraphQLResponseBuilder },
// required inputs for determining the response
])(payload, req.headers, req.contentType, streamOrSinglePayloadFromExecute)
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.
@maraisr You can prevent it to parse GET requests by a simple condition. You're still in charge here to decide when the request should be parsed.
if (request.method !== 'GET') {
const params = await getGraphQLParameters(request);
}
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.
6 lines of code included that won't be used. Death by a thousand cuts. Think optimally.
@@ -0,0 +1,17 @@ | |||
// returns the byte length of an utf8 string | |||
export function calculateByteLength(str: string): number { |
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.
can just be TextEncoder
in browsers/workers and from util
on node. No reason to include this.
aka just expose a helix/node-preamble
sub-module to do this work for the user.
eg:
const Encoder = new TextEncoder();
export function calculateByteLength(str: string): number {
return Encoder.encode(str).byteLength;
}
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 benchmarked it and it was so slow on Node.js
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.
So the web/browser has to pay the price? chuck it in a preamble or bundle for the different targets.
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.
Actually encoding entire string with TextEncoder sounds more expensive, 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.
One would have to benchmark, but in reality who cares about the byte length in the first place? Its a header that nobody is looking at.
Based on my other point in this thread, why does even need to exist in the first instance.
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 nobody refers to the HTTP Client here. As I already said below, some environments return 411 in case of missing length. Also this calculation was already in Helix before so this PR doesn't cover that actually.
headers: headersInit, | ||
status: 200, | ||
}; | ||
const readableStream = new ReadableStream({ |
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.
ReadableStreams isnt support in workers yet, may i suggest using https://github.com/maraisr/piecemeal here for this.
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.
Could you share a source that shows that? I'd love to see it.
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.
Just try it :)
"Content-Type": 'multipart/mixed; boundary="-"', | ||
"Transfer-Encoding": "chunked", | ||
}; | ||
const responseInit: ResponseInit = { |
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.
responseInit should be exposed through an argument, as a user may want to flush extra headers. eg https://github.com/maraisr/piecemeal/blob/2d9121904c33d0d0e6f606752f7250d590e2991b/src/worker.ts#L16-L17
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.
User can do it with response.headers.append
method.
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.
Headers, but what about other properties? And fairly sure once a response is created its immutable, or CF throws a hissy fit.
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.
You can copy the parts of the Response object and create your own. Adding this kind of configurations will make this function overcomplicated so I'll keep it in that way.
const contentLength = calculateByteLength(chunk); | ||
const data = [ | ||
"", | ||
"Content-Type: application/json; charset=utf-8", | ||
"Content-Length: " + contentLength.toString(), |
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.
const contentLength = calculateByteLength(chunk); | |
const data = [ | |
"", | |
"Content-Type: application/json; charset=utf-8", | |
"Content-Length: " + contentLength.toString(), | |
const data = [ | |
"", | |
"Content-Type: application/json; charset=utf-8", |
content-lengths in multipart segments isnt required, and isnt in the http graphql spec either.
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 it is required in some env.
#145 (comment)
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.
Not in multipart responses, those are effectively "body" and is no mans land. The spec clearly stipulates its not required. Had the exact same learnings during my build of meros.
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.
It is not spec but the HTTP Client itself. Not required doesn't mean we shouldn't provide it. I think it is good to have this.
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.
Its not good, you have no grounds for that. Multipart isnt part of the http header anymore. Its 100% superficial 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.
This is not something this PR covers. This part was already there before this PR.
new Request(fullUrl, { | ||
method: "POST", | ||
body: JSON.stringify(maybeParsedBody), | ||
}).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.
request.clone().body
is gonna be faster, and less code right.
https://developer.mozilla.org/en-US/docs/Web/API/Request/clone
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 actually a hack for NodeJS(just to have the same output with other envs). We don't have body in the request on purpose above because it might be GET
request so there is nothing to clone actually.
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 should nodejs be having any impact for consumers of other targets? Apply the "nodejs hacks" for the nodejs target, not for the workers or others.
Granted its in the node utils file.
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 applied already in a NodeJS helper. Check the name of the function, it has NodeRequest :) So other consumers don't call this function at all.
const encodedChunk = encodeString(chunk); | ||
controller.enqueue('\r\n'); | ||
controller.enqueue('Content-Type: application/json; charset=utf-8\r\n'); | ||
controller.enqueue('Content-Length: ' + encodedChunk.byteLength + '\r\n'); |
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.
Again, absolutely 100% zero need or anything for this line to exist. The body isn't being inspect by any cdn or intermediary.
I mean this line is effectively the same as returning json, and the cdn have a whinge coz there was no closing bracket.
It's the body, and those things aren't snooped.
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.
As I already said above, this is not something this PR covers.
This shared effort to support W3C request/response normalization has been moved to: dotansimha/graphql-yoga#776 The main reason is the fact that this change the nature of Helix and the goals of it (since it introduces a different setup, and aims to provide an agnostic handler for any env, while Helix, at the moment, is node/deno only). This concept and implementation will land in next major version of Yoga. |
Continue the work from: #94