Skip to content

Conversation

@cstoltze
Copy link
Contributor

@cstoltze cstoltze commented Jan 8, 2026

Update the graphql yoga handler to allow cedar users to enable the useExecutionCancellation plugin using extraPlugins.

Execution cancellation is useful to stop resolving data on the server when the client is no longer interested in the response. This PR sets up the graphql handler so that this plugin is available if cedar users want it.

See graphql-yoga migration notes for justification of why the handler needs to change in order for this plugin to function correctly. handleNodeRequest() is also deprecated:

(method) ServerAdapterObject<{}>.handleNodeRequest(
  nodeRequest: NodeRequest, 
  ...ctx: Partial<ServerAdapterInitialContext>[]
): MaybePromise<Response>
This function takes Node’s request object and returns a WHATWG Fetch spec compliant Response object.
@deprecated — Use handleNodeRequestAndResponse instead.

@netlify
Copy link

netlify bot commented Jan 8, 2026

Deploy Preview for cedarjs ready!

Name Link
🔨 Latest commit 18e8a64
🔍 Latest deploy log https://app.netlify.com/projects/cedarjs/deploys/69615d7422f94900085a8bf8
😎 Deploy Preview https://deploy-preview-957--cedarjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Greptile Summary

Updated GraphQL Yoga handler to use handleNodeRequestAndResponse instead of handleNodeRequest, which is necessary for enabling execution cancellation support.

Critical Issue Found:

  • The implementation incorrectly attempts to manually write the response after calling handleNodeRequestAndResponse. According to the GraphQL Yoga v5 migration notes, handleNodeRequestAndResponse automatically writes to the response object, so the manual header/status/body operations (lines 104-109) are redundant and will cause double-writing issues.

Additional Observation:

  • The useExecutionCancellation plugin mentioned in the PR description is not actually being added to the plugin list in createGraphQLYoga.ts. Simply changing the method name enables the capability, but the plugin should be explicitly added if execution cancellation is desired.

Confidence Score: 2/5

  • This PR has a critical implementation bug that will cause runtime errors with double response writing
  • The change from handleNodeRequest to handleNodeRequestAndResponse is correct in principle, but the implementation is fundamentally broken. The code still manually writes headers, status, and body after calling handleNodeRequestAndResponse, which automatically handles this. This will likely cause errors like "Cannot set headers after they are sent" or response corruption. Additionally, the useExecutionCancellation plugin is not being added anywhere.
  • packages/api-server/src/plugins/graphql.ts requires immediate correction to remove redundant response writing

Important Files Changed

File Analysis

Filename Score Overview
packages/api-server/src/plugins/graphql.ts 2/5 Updated to handleNodeRequestAndResponse but incorrectly still manually writing response, which will cause double-writing issues

Sequence Diagram

sequenceDiagram
    participant Client
    participant Fastify
    participant GraphQLHandler
    participant Yoga
    participant Reply
    
    Client->>Fastify: HTTP Request
    Fastify->>GraphQLHandler: route handler(req, reply)
    GraphQLHandler->>Yoga: handleNodeRequestAndResponse(req, reply, context)
    Note over Yoga,Reply: Yoga writes response directly to reply object
    Yoga->>Reply: Set headers, status, body
    Yoga-->>GraphQLHandler: Returns Response object
    Note over GraphQLHandler: ⚠️ ISSUE: Code tries to write again
    GraphQLHandler->>Reply: reply.header() - redundant
    GraphQLHandler->>Reply: reply.status() - redundant
    GraphQLHandler->>Reply: reply.send() - redundant
    GraphQLHandler-->>Fastify: return reply
    Fastify-->>Client: HTTP Response
Loading

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 8, 2026

Additional Comments (1)

packages/api-server/src/plugins/graphql.ts
handleNodeRequestAndResponse automatically writes the response to the reply object. The manual header setting, status setting, and body sending (lines 104-109) should be removed. According to the GraphQL Yoga migration notes, when using handleNodeRequestAndResponse, the response is already written and you should just return the reply object.

      const response = await yoga.handleNodeRequestAndResponse(req, reply, {
        req,
        reply,
        event: lambdaEventForFastifyRequest(req),
        requestContext: {},
      })

      return reply

Comment on lines 93 to 112
const graphQLYogaHandler = async (
req: FastifyRequest,
reply: FastifyReply,
) => {
const response = await yoga.handleNodeRequest(req, {
const response = await yoga.handleNodeRequestAndResponse(req, reply, {
req,
reply,
event: lambdaEventForFastifyRequest(req),
requestContext: {},
})

for (const [name, value] of response.headers) {
reply.header(name, value)
}

reply.status(response.status)
reply.send(response.body)

return reply
}
Copy link
Contributor Author

@cstoltze cstoltze Jan 8, 2026

Choose a reason for hiding this comment

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

I'm unsure if there are other implications of this change for cedar internals.

The hope is that this is a starting point for investigation

@Tobbe
Copy link
Member

Tobbe commented Jan 8, 2026

@cstoltze Thanks for getting started on this PR! Did you read Graphile's Summary? Is anything it says valid?

@nx-cloud
Copy link

nx-cloud bot commented Jan 8, 2026

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 18e8a64

Command Status Duration Result
nx run-many -t build:pack --exclude create-ceda... ✅ Succeeded 2s View ↗
nx run-many -t test --minWorkers=1 --maxWorkers=4 ✅ Succeeded 2m 43s View ↗
nx run-many -t test:types ✅ Succeeded 10s View ↗
nx run-many -t build ✅ Succeeded 1m 31s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-09 20:16:58 UTC

@nx-cloud
Copy link

nx-cloud bot commented Jan 8, 2026

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 94cb0f3

Command Status Duration Result
nx run-many -t build ✅ Succeeded 14s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-08 17:46:33 UTC

@nx-cloud
Copy link

nx-cloud bot commented Jan 8, 2026

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 94cb0f3

Command Status Duration Result
nx run-many -t build ✅ Succeeded 3s View ↗
nx run-many -t build:pack --exclude create-ceda... ✅ Succeeded 7s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-08 17:49:21 UTC

@Tobbe Tobbe changed the title enable execution cancellation feat(gql): enable execution cancellation Jan 8, 2026
@cstoltze
Copy link
Contributor Author

cstoltze commented Jan 8, 2026

@cstoltze Thanks for getting started on this PR! Did you read Graphile's Summary? Is anything it says valid?

@Tobbe Thanks for taking a look at this! I'm no expert on cedar internals, so will defer to your judgement.

I did read greptile's summary and I do not think it's raising legitimate concerns.

The example fastify app for graphql yoga looks like this which seems identical to what is present in cedar. So I think this should work as-is.

  const handler = async (req, reply) => {
    const response = await graphQLServer.handleNodeRequestAndResponse(req, reply, {
      req,
      reply,
    });
    for (const [key, value] of response.headers.entries()) {
      reply.header(key, value);
    }

    reply.status(response.status);

    reply.send(response.body);

    return reply;
  };

@Tobbe
Copy link
Member

Tobbe commented Jan 8, 2026

The example fastify app for graphql yoga looks like this which seems identical to what is present in cedar.

I think this might be a better example to look at

https://github.com/graphql-hive/graphql-yoga/blob/083d65163a0d4868f0547c06fa366da6b4013eaa/examples/fastify/src/app.ts#L18

If we wanted to enable request cancellation out of the box, we'd need to include that plugin.

Another option would be to only switch to handleNodeRequestAndResponse, and then let users add the useExecutionCancellation plugin themselves by adding it to the extraPlugins option.

@Tobbe Tobbe force-pushed the main branch 5 times, most recently from 0f0e039 to aa1c62d Compare January 8, 2026 23:14
@cstoltze
Copy link
Contributor Author

cstoltze commented Jan 8, 2026

Thanks for finding the better reference file!

If we wanted to enable request cancellation out of the box, we'd need to include that plugin.

Another option would be to only switch to handleNodeRequestAndResponse, and then let users add the useExecutionCancellation plugin themselves by adding it to the extraPlugins option

This is a good point. I was definitely thinking more along the lines of using extraPlugins when suggesting this, but including request cancellation out of the box would be even better. Do you think most cedar users would want it?

Comment on lines -104 to -113
for (const [name, value] of response.headers) {
reply.header(name, value)
}

reply.status(response.status)
reply.send(response.body)

return reply
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Tobbe
Copy link
Member

Tobbe commented Jan 9, 2026

I was definitely thinking more along the lines of using extraPlugins when suggesting this

Great, because that's what I'd want. At least for now. I just wasn't sure with the title of the PR and the description.

How about we merge this PR with the code changes like they are right now, and then after you guys have tried it out with useExecutionCancellation we can revisit if it should be included by default or not? I'll probably rewrite the PR description in that case to make it clear that the PR doesn't actually enable execution cancellation, only the possibility of adding it if wanted.

@cstoltze
Copy link
Contributor Author

cstoltze commented Jan 9, 2026

Great, because that's what I'd want. At least for now. I just wasn't sure with the title of the PR and the description.

Perfect! Yes, sorry for the imprecise naming.

How about we merge this PR with the code changes like they are right now, and then after you guys have tried it out with useExecutionCancellation we can revisit if it should be included by default or not? I'll probably rewrite the PR description in that case to make it clear that the PR doesn't actually enable execution cancellation, only the possibility of adding it if wanted.

This sounds good. Yes please rewrite the description/title of the PR to better reflect this.

We will keep you updated on how this works when we try it out. We're currently upgrading to redwood latest and plan to migrate to cedar shortly afterwards. This functionality is one of many reasons we're interested in moving to cedar.

Thanks again for looking at this! We really appreciate your assistance on this and your efforts on cedar more generally.

@Tobbe Tobbe changed the title feat(gql): enable execution cancellation fix(gql): switch to using handleNodeRequestAndResponse Jan 9, 2026
@Tobbe
Copy link
Member

Tobbe commented Jan 9, 2026

@greptileai Please do a full review of this PR again and update your review Summary

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Greptile Summary

This PR switches from handleNodeRequest to handleNodeRequestAndResponse and removes the manual response handling code that was incorrectly duplicating what handleNodeRequestAndResponse already does internally.

Key Changes:

  • Switches from yoga.handleNodeRequest() to yoga.handleNodeRequestAndResponse()
  • Removes manual header copying, status setting, and body sending
  • Simplifies the handler to an arrow function expression

Context:
This change enables GraphQL Yoga's execution cancellation feature (as noted in commit 94cb0f3). The handleNodeRequestAndResponse method automatically writes the response to the Fastify reply object, making the manual response handling in the old code redundant and potentially problematic (could cause double-write issues).

Migration Reference:
The implementation follows the GraphQL Yoga 5.x migration pattern documented in graphql-hive/graphql-yoga#3215 and matches their official Fastify example.

Confidence Score: 4/5

  • Safe to merge - correctly implements GraphQL Yoga 5.x API with minor style improvement recommended
  • The PR correctly switches to handleNodeRequestAndResponse and removes the problematic manual response handling that would have caused issues. The implementation follows GraphQL Yoga's official migration pattern. The only concern is a minor style issue where the handler should explicitly return reply following Fastify conventions, but this is non-blocking as the response is already sent internally.
  • No files require special attention - the single file change is straightforward and correct

Important Files Changed

File Analysis

Filename Score Overview
packages/api-server/src/plugins/graphql.ts 4/5 Switches to handleNodeRequestAndResponse and removes manual response handling. The handler should explicitly return reply for proper Fastify integration.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Fastify
    participant Handler as graphQLYogaHandler
    participant Yoga as yoga.handleNodeRequestAndResponse
    participant Reply as FastifyReply

    Client->>Fastify: HTTP Request
    Fastify->>Handler: route handler(req, reply)
    Handler->>Yoga: handleNodeRequestAndResponse(req, reply, context)
    
    Note over Yoga: Processes GraphQL request
    Note over Yoga: Parses query
    Note over Yoga: Executes resolvers
    Note over Yoga: Formats response
    
    Yoga->>Reply: Sets headers
    Yoga->>Reply: Sets status code
    Yoga->>Reply: Calls reply.send(body)
    
    Yoga-->>Handler: Returns (reply already sent)
    Handler-->>Fastify: Returns result
    Fastify-->>Client: HTTP Response
Loading

Comment on lines 93 to 102
const graphQLYogaHandler = async (
req: FastifyRequest,
reply: FastifyReply,
) => {
const response = await yoga.handleNodeRequest(req, {
) =>
yoga.handleNodeRequestAndResponse(req, reply, {
req,
reply,
event: lambdaEventForFastifyRequest(req),
requestContext: {},
})
Copy link
Contributor

Choose a reason for hiding this comment

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

[P3] While handleNodeRequestAndResponse writes the response internally, Fastify handlers should explicitly return reply when the reply has been sent. This ensures proper type checking and follows Fastify best practices.

Suggested change
const graphQLYogaHandler = async (
req: FastifyRequest,
reply: FastifyReply,
) => {
const response = await yoga.handleNodeRequest(req, {
) =>
yoga.handleNodeRequestAndResponse(req, reply, {
req,
reply,
event: lambdaEventForFastifyRequest(req),
requestContext: {},
})
const graphQLYogaHandler = async (
req: FastifyRequest,
reply: FastifyReply,
) => {
await yoga.handleNodeRequestAndResponse(req, reply, {
req,
reply,
event: lambdaEventForFastifyRequest(req),
requestContext: {},
})
return reply
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/api-server/src/plugins/graphql.ts
Line: 93:102

Comment:
[P3] While `handleNodeRequestAndResponse` writes the response internally, Fastify handlers should explicitly return `reply` when the reply has been sent. This ensures proper type checking and follows Fastify best practices.

```suggestion
    const graphQLYogaHandler = async (
      req: FastifyRequest,
      reply: FastifyReply,
    ) => {
      await yoga.handleNodeRequestAndResponse(req, reply, {
        req,
        reply,
        event: lambdaEventForFastifyRequest(req),
        requestContext: {},
      })

      return reply
    }
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@Tobbe
Copy link
Member

Tobbe commented Jan 9, 2026

Looking at the example here https://the-guild.dev/graphql/yoga-server/docs/integrations/integration-with-fastify#example they're only passing req and reply in the third argument:

yoga.handleNodeRequestAndResponse(req, reply, {
  req,
  reply
})

It might be fine. It's what we did before too, when using handleNodeRequest(). But it'd be nice to know what's going on here

@Tobbe Tobbe added the release:fix This PR is a fix label Jan 9, 2026
@Tobbe Tobbe added this to the next-release-patch milestone Jan 9, 2026
@Tobbe
Copy link
Member

Tobbe commented Jan 9, 2026

I added types to the createYoga call according to the example here: https://the-guild.dev/graphql/yoga-server/docs/integrations/integration-with-fastify#example

And now, with how simple the handler has become, I figured we might as well just inline it, which also makes it follow that same example much closer

Assuming CI passes I'm going to merge this PR now despite still not fully understanding the extra event and requestContext parameters

@Tobbe Tobbe enabled auto-merge (squash) January 9, 2026 19:56
@Tobbe Tobbe merged commit bd6d9a1 into cedarjs:main Jan 9, 2026
42 checks passed
@Tobbe
Copy link
Member

Tobbe commented Jan 9, 2026

I'm now more confident in the event and requestContext parameters. I did some more investigating/tracing the code and followed up with this PR: #969

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:fix This PR is a fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants