Skip to content
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

Changes to the incremental responses #204

Merged

Conversation

jer-k
Copy link
Contributor

@jer-k jer-k commented Jul 19, 2024

Fixes: #203

Preface: This is my first exposure to working with these Node objects on the server side so I'm not entirely sure if what I've done is correct.

I changed the way the incremental responses are sent back to the client. In the issue I noted

      for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
        body.push(chunk);
      }

this await is stopping the server from incrementally delivering the responses and instead they're all delivered at once.

These changes allow the responses to be streamed back instead of all at once.

The logs in the client now show

what is data? {book: {…}} false 12:22:18
what is data? {book: {…}} false 12:22:19

Where the second book: {} has the deferred data and is delivered 1 second after the first in which the 1 second is hard coded in my project using this package.

@jer-k jer-k requested a review from a team as a code owner July 19, 2024 19:24
@jer-k
Copy link
Contributor Author

jer-k commented Jul 19, 2024

A couple things I've noticed. With these changes the Preview and Response panes in the Chrome DevTools aren't showing any data even though the response is working

image
image
image

I'm guessing something I've changed messed this up.

@@ -50,6 +50,10 @@ function startServerAndCreateNextHandler<
if (httpGraphQLResponse.body.kind === 'complete') {
res.send(httpGraphQLResponse.body.string);
} else {
res.writeHead(200, {
'Content-Type': 'multipart/mixed; boundary="-"',
Copy link
Contributor Author

@jer-k jer-k Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this header isn't set, then Apollo Client throws an error that looks like this

Error: No number after minus sign in JSON at position 3 (line 2 column 2)

Looking at the data being sent, it lines up with the ---
image

After setting it to 'multipart/mixed; boundary="-"' the Preview and Response panels display nothing, as I mentioned in the other comment. Not sure why this is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking below, where I actually spread in the existing headers

      headers: {
        ...headers,
        'Content-Type': 'application/json',
        'Transfer-Encoding': 'chunked',
      },

those requests have multipart/mixed; boundary="-"; deferSpec=20220824 so I probably need to do something similar here instead of hard coding

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this by ensuring the passed in headers are applied in all cases

@martinnabhan
Copy link
Collaborator

Thank you for the thorough explanation! I'll try to take a closer look this week.

@martinnabhan martinnabhan force-pushed the streaming-responses branch from 82f0180 to 36d9317 Compare July 29, 2024 09:35
Copy link
Collaborator

@martinnabhan martinnabhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some suggestions! Could you also remove noIncrementalDelivery: true from src/__tests__/integration.test.ts so we make sure to test incremental delivery as well?

I also noticed Chrome's Preview and Response panes aren't showing any data but I think that's normal for multipart responses.

const headers: Record<string, string> = {};
for (const [key, value] of httpGraphQLResponse.headers) {
headers[key] = value;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since headers isn't used in the if (isNextApiRequest(req)) code block it would be better to move this below the if statement.

@@ -50,6 +55,8 @@ function startServerAndCreateNextHandler<
if (httpGraphQLResponse.body.kind === 'complete') {
res.send(httpGraphQLResponse.body.string);
} else {
res.writeHead(200, headers);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary if we use res.send below.

Comment on lines 76 to 81
async start(controller) {
for await (const chunk of responseBody.asyncIterator) {
controller.enqueue(chunk);
}
controller.close();
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more common to use pull, and then use the iterator without looping like this:

Suggested change
async start(controller) {
for await (const chunk of responseBody.asyncIterator) {
controller.enqueue(chunk);
}
controller.close();
},
async pull(controller) {
if (httpGraphQLResponse.body.kind === 'chunked') {
const { value, done } = await httpGraphQLResponse.body.asyncIterator.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
}
},

src/startServerAndCreateNextHandler.ts Show resolved Hide resolved
@@ -50,6 +55,8 @@ function startServerAndCreateNextHandler<
if (httpGraphQLResponse.body.kind === 'complete') {
res.send(httpGraphQLResponse.body.string);
} else {
res.writeHead(200, headers);

for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should support incremental responses for API routes as well. We can just replace the for loop with this:

res.send(Readable.from(httpGraphQLResponse.body.asyncIterator));

@@ -59,24 +66,27 @@ function startServerAndCreateNextHandler<
return;
}

const body = [];
let responseObject: string | ReadableStream;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if we could use a ternary instead of a re-assignable variable, like this:

    return new Response(
      httpGraphQLResponse.body.kind === 'complete'
        ? httpGraphQLResponse.body.string
        : new ReadableStream({ ... }),
      { headers, status: httpGraphQLResponse.status || 200 },
    );

…ses back instead of batching them all together
@jer-k jer-k force-pushed the streaming-responses branch from 36d9317 to 2288ed3 Compare July 29, 2024 15:04
@jer-k
Copy link
Contributor Author

jer-k commented Jul 29, 2024

Thank you for all the feedback! It is a much cleaner implementation.

On the Preview panel, there definitely is some way for the data to show up. This screenshot is from my work's application, which uses Apollo Client + graphql-ruby. I'll spend some time this morning and see if I can't uncover any information as to what is going on

image

@jer-k
Copy link
Contributor Author

jer-k commented Jul 30, 2024

Okay I figured it out and I believe it is an upstream issue in Apollo Server and improperly setting the header.

what are raw headers? HeaderMap(2) [Map] {
  'cache-control' => 'no-store',
  'content-type' => 'multipart/mixed; boundary="-"; deferSpec=20220824',
  __identity: Symbol(HeaderMap)
}

When the headers are written in and the deferSpec=20220824', portion is retained, we do not get the Preview response.

I changed the code to

    const headers: Record<string, string> = {
      'content-type': 'multipart/mixed; boundary="-"',
    };
    // console.log('what are raw headers?', httpGraphQLResponse.headers);
    // for (const [key, value] of httpGraphQLResponse.headers) {
    //   headers[key] = value;
    // }

And the preview correctly displays
image

@martinnabhan
Copy link
Collaborator

Thank you for looking into this. I don't think we want to change any headers coming from Apollo Server, so I'll go ahead and add a changeset and release this as is.

Thank you for the pull request!

@martinnabhan martinnabhan merged commit e7b02be into apollo-server-integrations:main Jul 31, 2024
2 checks passed
@github-actions github-actions bot mentioned this pull request Jul 31, 2024
@jer-k jer-k deleted the streaming-responses branch July 31, 2024 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Streaming responses (@defer) waits on the server instead of incrementally delivering responses
2 participants