Skip to content

feat: allow multipart file uploads to be consumed as streams#4424

Draft
Copilot wants to merge 11 commits into
mainfrom
copilot/allow-multipart-uploads-as-streams
Draft

feat: allow multipart file uploads to be consumed as streams#4424
Copilot wants to merge 11 commits into
mainfrom
copilot/allow-multipart-uploads-as-streams

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

  • Understand current state of the PR and all reviewer comments
  • Fix useless done = true assignments at lines 347 and 351 in post-multipart.ts
  • Make streaming the default mode (remove non-streaming formData() code path, remove stream?: boolean option)
  • Add limits option to MultipartOptions and enforce them in the streaming parser (fileSize, files, fieldSize)
  • Add lazy-loading test to verify files are streamed not buffered
  • Update docs to reflect streaming-as-default and cross-platform limits configuration
  • Deduplicate error object creation in file size limit check
  • Fix docs: clarify disk-write examples are Node.js-specific
  • Fix CI: update incremental-delivery.spec.ts to use new multipart.limits API instead of fetchAPI: createFetch({ formDataLimits })
  • Fix CI: align error message format in streaming parser with old ponyfill format (File size limit exceeded: N bytes)

@codesandbox
Copy link
Copy Markdown

codesandbox Bot commented Mar 30, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@@ -1,3 +1,5 @@
import { Readable } from 'node:stream';
import Busboy, { BusboyFileStream } from '@fastify/busboy';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot We don't want this to be Node.js specific.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Replaced the @fastify/busboy + node:stream implementation with a self-contained WHATWG-streams-based multipart parser that uses only standard APIs (ReadableStream, TextDecoder, TextEncoder, Uint8Array). It works in Node.js, Cloudflare Workers, Deno, and browsers alike. The @fastify/busboy dependency has been removed from package.json. Commit: 040cdf1.

Copilot AI changed the title [WIP] Add support for processing multipart uploads as streams feat: allow multipart file uploads to be consumed as streams Mar 30, 2026
Copilot AI requested a review from ardatan March 30, 2026 13:24
Comment thread packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts Fixed
Comment thread packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts Fixed
@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://59469410.envelop.pages.dev

Comment thread packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts Fixed
Comment thread packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts Fixed
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • checkpoint.prisma.io
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node /home/REDACTED/work/graphql-yoga/graphql-yoga/node_modules/.pnpm/prisma@6.19.0_typescript@5.9.3/node_modules/prisma/build/child {"product":"prisma","version":"6.19.0","cli_install_type":"local","information":"","local_timestamp":"2026-03-30T13:36:40Z","project_hash":"75b1e306","cli_path":"/home/REDACTED/work/graphql-yoga/graphql-yoga/examples/hackernews/node_modules/prisma/build/ind (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Copy Markdown
Member

@ardatan ardatan left a comment

Choose a reason for hiding this comment

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

Make sure CI doesn't fail

@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://f4241516.envelop.pages.dev

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

Make sure CI doesn't fail

Fixed. The CI failure was a TypeScript 5.9 error — Uint8Array<ArrayBufferLike> (returned by ReadableStreamDefaultReader.read()) was not assignable to Uint8Array<ArrayBuffer> (the previous parameter type of concatBytes/indexOfBytes). Updated both helper functions to accept Uint8Array<ArrayBufferLike>. Commit: 70473b3.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • checkpoint.prisma.io
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node /home/REDACTED/work/graphql-yoga/graphql-yoga/node_modules/.pnpm/prisma@6.19.0_typescript@5.9.3/node_modules/prisma/build/child {&#34;product&#34;:&#34;prisma&#34;,&#34;version&#34;:&#34;6.19.0&#34;,&#34;cli_install_type&#34;:&#34;local&#34;,&#34;information&#34;:&#34;&#34;,&#34;local_timestamp&#34;:&#34;2026-03-30T13:51:38Z&#34;,&#34;project_hash&#34;:&#34;75b1e306&#34;,&#34;cli_path&#34;:&#34;/home/REDACTED/work/graphql-yoga/graphql-yoga/examples/hackernews/node_modules/prisma/build/ind (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Comment thread packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts Fixed
Comment thread packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts Fixed
Copilot AI requested a review from ardatan March 30, 2026 14:00
}
});

describe('file uploads (streaming mode)', () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot make sure it is streamed not buffered first then consumed. So files should be lazily loaded right? So let's test it

@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://6ff8a251.envelop.pages.dev

@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://pr-4424.graphql-yoga.pages.dev

- **Only available in Node.js.** In edge runtimes the standard `request.formData()` path is used
regardless of this option.

## Configuring Multipart Request Processing (only for Node.js)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot Implement this in the parser too so we don't have this node.js specific and have this part of our new parser. Update the configuraiton api etc for that

@theguild-bot
Copy link
Copy Markdown
Collaborator

theguild-bot commented Mar 30, 2026

✅ Yoga Benchmark Results

     ✓ no_errors{mode:graphql}
     ✓ expected_result{mode:graphql}
     ✓ no_errors{mode:graphql-jit}
     ✓ expected_result{mode:graphql-jit}
     ✓ no_errors{mode:graphql-response-cache}
     ✓ expected_result{mode:graphql-response-cache}
     ✓ no_errors{mode:graphql-no-parse-validate-cache}
     ✓ expected_result{mode:graphql-no-parse-validate-cache}
     ✓ no_errors{mode:uws}
     ✓ expected_result{mode:uws}

     checks.......................................: 100.00% ✓ 683308      ✗ 0     
     data_received................................: 2.8 GB  18 MB/s
     data_sent....................................: 116 MB  774 kB/s
     http_req_blocked.............................: avg=1.38µs   min=901ns    med=1.18µs   max=336.46µs p(90)=1.78µs   p(95)=2.06µs  
     http_req_connecting..........................: avg=2ns      min=0s       med=0s       max=157.84µs p(90)=0s       p(95)=0s      
     http_req_duration............................: avg=355.26µs min=214.85µs med=337.65µs max=19.81ms  p(90)=474.45µs p(95)=494.41µs
       { expected_response:true }.................: avg=355.26µs min=214.85µs med=337.65µs max=19.81ms  p(90)=474.45µs p(95)=494.41µs
     ✓ { mode:graphql-jit }.......................: avg=272.33µs min=214.85µs med=263.55µs max=19.81ms  p(90)=292.56µs p(95)=306.61µs
     ✓ { mode:graphql-no-parse-validate-cache }...: avg=496.95µs min=425.93µs med=479.29µs max=6.94ms   p(90)=516.92µs p(95)=541.15µs
     ✓ { mode:graphql-response-cache }............: avg=335.75µs min=266.61µs med=325.31µs max=9.87ms   p(90)=363.12µs p(95)=377.91µs
     ✓ { mode:graphql }...........................: avg=368.65µs min=296.07µs med=350.49µs max=16.48ms  p(90)=396.06µs p(95)=435.15µs
     ✓ { mode:uws }...............................: avg=358.29µs min=291.26µs med=344.85µs max=6.43ms   p(90)=374.98µs p(95)=388.98µs
     http_req_failed..............................: 0.00%   ✓ 0           ✗ 341654
     http_req_receiving...........................: avg=32.86µs  min=16.47µs  med=31.98µs  max=3.47ms   p(90)=38.51µs  p(95)=40.93µs 
     http_req_sending.............................: avg=8.21µs   min=5.7µs    med=7.06µs   max=452.89µs p(90)=11.25µs  p(95)=13.69µs 
     http_req_tls_handshaking.....................: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting.............................: avg=314.18µs min=182.66µs med=298.27µs max=19.69ms  p(90)=433.16µs p(95)=452.48µs
     http_reqs....................................: 341654  2277.666358/s
     iteration_duration...........................: avg=434.2µs  min=280.81µs med=414.67µs max=20.43ms  p(90)=554.64µs p(95)=575.83µs
     iterations...................................: 341654  2277.666358/s
     vus..........................................: 1       min=1         max=1   
     vus_max......................................: 2       min=2         max=2   

@phal0r
Copy link
Copy Markdown

phal0r commented Apr 10, 2026

hey @ardatan ,

I saw you started working on this. In the meantime I implemented with help of Claude a GatewayPlugin and a YogaPlugin, that allows for streaming. In case you are interested, I can share the code.

@ardatan
Copy link
Copy Markdown
Member

ardatan commented Apr 10, 2026

Sure @phal0r ! I'd love to see it!

@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://e8454ba2.envelop.pages.dev

@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://pr-4424.graphql-yoga.pages.dev

@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://ba9b7d0a.envelop.pages.dev

@theguild-bot
Copy link
Copy Markdown
Collaborator

💻 Website Preview

The latest changes are available as preview in: https://pr-4424.graphql-yoga.pages.dev

@phal0r
Copy link
Copy Markdown

phal0r commented Apr 13, 2026

Sure thing @ardatan !

Some additional notes: I implemented one GatewayPlugin for the hive-gateway and one YogaPlugin for the yoga-graphql server, although I know that the gateway uses yoga under the hood. The implementations are different due to different contexts: For the gateway I assume, that all subgraphs run in the same cluster/network/infra, so usually network is stable, fast and reliable. Thus the stream is just piped through from incoming network to outgoing network. For yoga I choose to write the file to disk and create a read stream from the temporarily stored file, because usually one wants to send this to some object storage with unreliable network over internet.

Of course this is only an inspiration for a final solution, but would love to see those implemented and remove my custom plugins :)

This is the one for Yoga: https://gist.github.com/phal0r/b25878bd83e10b576529f59f6bbb16fc

this is the one for the Gateway: https://gist.github.com/phal0r/507e7cd3b67230b2fd7fbc14efff9da0

I am running both apps with Bun and it works well there. I tested with files > 1GB and container limits of 1GB to ensure it really works. I also created some end2end tests with real file uploads through the network to rule out any inconsistencies.

Along the way were other problems, like the minio js client offering streaming, but being buggy (does not cater for backpressure from the network and thus still loaded the whole in a memory buffer :D )

At the moment this system runs reliable in my tests with big uploads.

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.

Allow to process multipart uploads as streams to reduce memory usage

4 participants