Skip to content

feat(node): add TmpFileUploadHandlerPlugin - #1863

Merged
dinwwwh merged 8 commits into
middleapi:mainfrom
dinwwwh:claude/node-body-parser-plugin-a9babe
Aug 15, 2026
Merged

feat(node): add TmpFileUploadHandlerPlugin#1863
dinwwwh merged 8 commits into
middleapi:mainfrom
dinwwwh:claude/node-body-parser-plugin-a9babe

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Aug 14, 2026

Copy link
Copy Markdown
Member

Adds TmpFileUploadHandlerPlugin to @orpc/node: file uploads and multipart file parts stream into per-request temporary files instead of memory, so requests far larger than available memory parse in constant memory. Kind-aware size limits let it replace the request limit plugin while sizing each kind of body to what it actually costs. Also upgrades @standardserver/* to 0.8.0 repo-wide and adopts its resolveStandardBodyHint, so the plugin decides body kinds exactly as the standard parsers do.

Behavior

  • Procedures receive lazily read TmpFile instances (a File exposing its backing path), so an upload can be kept with a cheap rename; files are removed when the request finishes.
  • Streaming response bodies, event iterators and raw streams, keep the tmp files alive until they complete, so a response can read the upload while transmitting; abandoned transfers clean up only after in-flight procedure logic finishes.
  • maxBodySize groups three limits that are required together: memory (JSON, urlencoded, multipart fields), file (content spooled to disk), and stream (content consumed on the fly). A declared content-length over the limit rejects before any byte is read, enforcement continues while streaming so a lying length cannot bypass it, and a multipart body as a whole is bounded by the memory and file limits combined so framing cannot hide bytes. Limit and storage errors carry no implementation details to the client, and filesystem failures keep their 500 status through the decode step's client-error mapping.
  • Multipart parsing is a dependency-free streaming parser aligned with the standard parser: entry order, WHATWG %22/%0D/%0A decoding, path-preserving filenames, UTF-8 field values, preserved content-type parameters, and strict rejection of malformed parts.

Testing

  • 136 unit tests, including differential fuzzing against Response.formData() down to 1-byte chunking with async delivery, drip-fed socket uploads, flat file-descriptor usage across 300-part bodies, and cleanup on every error path.
  • Integration suite covers 32MB uploads through RPCLink, composition with the request compression and request limit plugins, streaming-response echo, and abort cleanup; the plugin joined the all-plugins matrix test.
  • Full monorepo suite, type check, lint, and docs:validate pass on the 0.8.0 upgrade; docs page added at docs/plugins/tmp-file-upload.

…up to TmpFileUploadHandlerPlugin

Adopts resolveStandardBodyHint from @standardserver/core 0.8.0 for body kind
resolution. Requests are limited per content category (memory, file, stream)
through a grouped maxBodySize option, subsuming the request limit plugin.
Tmp file removal defers onto streaming response bodies so they can read the
upload while transmitting, and waits for in-flight logic on abandoned
transfers. Limit and storage errors carry no implementation details to the
client, while filesystem failures keep their server-fault status through the
decode step's client-error mapping.
@pullfrog

pullfrog Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview Aug 15, 2026 3:31am

@pkg-pr-new

pkg-pr-new Bot commented Aug 14, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1863

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1863

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1863

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1863

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1863

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1863

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1863

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1863

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@1863

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1863

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1863

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1863

@orpc/node

npm i https://pkg.pr.new/@orpc/node@1863

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1863

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1863

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1863

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1863

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1863

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1863

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1863

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1863

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@1863

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1863

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1863

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1863

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1863

commit: 32ae030

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing dinwwwh:claude/node-body-parser-plugin-a9babe (32ae030) with main (2e405dd)

Open in CodSpeed

One shared harness replaces three duplicated interceptor drivers, and new
cases cover every remaining branch: bodiless delegation, default options,
non-stream adapter passthroughs on each body kind, file bodies without
content headers, form-data hints without a content type, append and seal
filesystem failures, and the parser's ignored headers and parameter-less
dispositions. packages/node/src sits at 100% statements, branches,
functions, and lines.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The incremental change (commit 7203188c) moves the maxBodySize configuration from the inline Setup code example into its own "Limiting Body Sizes" section with a dedicated code block. The text is polished and the structure is clearer — no behavioral code changed.

  • Moved maxBodySize JSDoc + config into a self-contained "Limiting Body Sizes" section
  • Polished prose throughout (grammar fixes, tighter wording)
  • Added cross-reference that the plugin subsumes the Request Limit Plugin when all three limits are configured

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…rt parser

The parser is form-data specific, so the serialization escapes of quotes and
line breaks in names and filenames decode where those parameters are
extracted, instead of leaving callers with encoded values.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

Single commit 327ccf13 tightens multipart part header parsing to match the standard parser's exact shape (RFC 9110 token header names, lowercase form-data disposition, quoted name/filename in order, bare-CR-as-line-break, last-wins repeated headers, strict rejection of filename*), with an adversarial differential test locking every edge case to Response.formData() live behavior.

  • Replaced parseHeaderParameters-based content-disposition parsing with a strict parseContentDisposition function matching standard-parser accept/reject behavior exactly
  • Added HEADER_NAME_REGEX validation per RFC 9110 tokens for all part header names
  • Added 29-case adversarial differential test plus an RFC 2046 leniency test
  • Updated parser JSDoc to document the now-exact contract with deliberate leniencies called out

The parser now rejects shapes the standard parser rejects (uppercase disposition type, non-token header names, filename*, whitespace-before-semicolon, out-of-order parameters), and its only leniencies (RFC 2046 preamble/epilogue/transport padding) are explicitly tested as such.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Audited against the grammar and other parsers (undici, busboy,
@fastify/busboy): malformed bodies, including boundary matches without a
delimiter tail, throw instead of growing reinterpretation machinery, matching
the standard parser. Parameters without a value skip cleanly, chunk intake
avoids a copy when nothing is retained, and tests pin the strict cases plus
the leniencies the comparison confirmed: header colon spacing, close-delimiter
junk, ignored filename*, and first-wins duplicate parameters.
@dinwwwh
dinwwwh force-pushed the claude/node-body-parser-plugin-a9babe branch from 35325ef to 32ae030 Compare August 15, 2026 03:30

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

Single commit 35325eff fixes the multipart parser's handling of boundary matches that aren't real delimiters (trailing tail is not CRLF or --). Instead of throwing, those bytes are now treated as ordinary content, matching the RFC grammar.

  • Replaced the throw in delimiter-end with a fallback that writes the delimiter back to the writer and continues scanning
  • Extended parseHeaderParameters to skip valueless parameters rather than breaking, matching first-wins semantics
  • Added 2 new regression test cases (boundary-as-content in body and preamble, cross-parser leniencies), changed 1 existing test (garbageAfterBoundary), and added parseHeaderParameters skip-without-value coverage
  • All 55 tests pass (18 multipart + 37 plugin), including the existing differential fuzzing suite

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The author force-pushed to replace commit 35325eff (tolerant boundary handling) with 32ae030a (strict parser). The key differences:

  • Strict reject, not tolerant: boundary matches not followed by CRLF or -- now throw rather than being treated as ordinary content. This matches the RFC grammar — a \r\n--boundary sequence in the body IS a delimiter, and its tail being anything other than CRLF/-- is a framing error.
  • Simpler state machine: writer.end() is called immediately when a delimiter match is found in body state (before tail confirmation), eliminating the stateBeforeDelimiter rollback logic. The previous guard on writer in the -- close check is also removed since writer is always undefined at delimiter-end.
  • Buffer copy optimization: when buffer is empty (flushed), Buffer.from(chunk.buffer, ...) is used instead of Buffer.concat to avoid a copy.
  • New test: falseDelimiterInBody validates the strict throw on body content containing a boundary-like pattern.
  • Test removed: the "treats boundary matches without a delimiter tail as ordinary content" test is gone with the tolerant approach.
  • All 54 tests pass (17 multipart + 37 plugin).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit a214208 into middleapi:main Aug 15, 2026
11 checks passed
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.

1 participant