Skip to content

feat(node): parse bodies from req.rawBody on Google Cloud Functions - #138

Closed
dinwwwh wants to merge 1 commit into
mainfrom
claude/nodejs-adapter-rawbody-gcf-cc4a63
Closed

dinwwwh wants to merge 1 commit into
mainfrom
claude/nodejs-adapter-rawbody-gcf-cc4a63

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 30, 2026

Copy link
Copy Markdown
Member

The node adapter now resolves request bodies correctly on Google Cloud Functions and Firebase HTTP functions. Their Functions Framework reads every body before the function runs and keeps the raw bytes on req.rawBody. Before this change, toStandardBody returned the framework's own req.body, so 5 of the 6 body types tested reached the handler as the wrong type. Once the stream has been read, the adapter now parses req.rawBody (any Uint8Array) exactly as it would parse the stream. Otherwise it still falls back to req.body.

Fixes (Functions Framework v5)

  • FormData, files, event streams and octet streams arrive as FormData, File, async iterators and ReadableStream instead of a Buffer
  • URLSearchParams bodies arrive as URLSearchParams instead of a plain object
  • text/plain files arrive as a File instead of a string

Behavior change

  • When an app saves req.rawBody itself (e.g. Express verify callbacks for webhook signatures), JSON is now parsed from those bytes instead of using its req.body. The result is identical unless the app changes req.body in between, for example with a JSON reviver.
  • A rawBody that isn't a Uint8Array (e.g. a string) is ignored.

Platform limits (not fixable in the adapter)

  • The Functions Framework buffers request bodies, so they can't stream. Responses still stream.
  • Its strict JSON parser rejects a top-level JSON primitive ("string", 1, null) with a 400 before the adapter runs.

Performance

  • Requests that carry no rawBody behave exactly as before.
  • Wrapping rawBody in a stream costs about 5µs per request. We kept that instead of adding a separate raw-bytes branch for each body type.

Testing

  • New tests/google-cloud-functions.test.ts sends json, url-search-params, file, form-data, event-stream and octet-stream bodies through the real Functions Framework. Five of the six fail against main. This adds @google-cloud/functions-framework as a root dev dependency.
  • New unit tests cover rawBody taking precedence over req.body, and a rawBody that isn't a Uint8Array being ignored.
  • Full suite (1336 tests), type:check and eslint pass.

Google Cloud Functions and Firebase HTTP functions read every request
body before the function runs and keep the raw bytes on `req.rawBody`.
`toStandardBody` returned their parsed `req.body` as-is, so FormData,
files, event streams and octet streams arrived as a Buffer,
URLSearchParams as a plain object, and text blobs as a string. Once the
stream is consumed, the adapter now parses `req.rawBody` (a Uint8Array)
the same way it parses the stream, falling back to `req.body` otherwise.
@pkg-pr-new

pkg-pr-new Bot commented Sep 30, 2026

Copy link
Copy Markdown
@standard-server/aws-lambda

npm i https://pkg.pr.new/@standard-server/aws-lambda@138

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@138

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@138

@standard-server/fetch

npm i https://pkg.pr.new/@standard-server/fetch@138

@standard-server/node

npm i https://pkg.pr.new/@standard-server/node@138

@standard-server/peer

npm i https://pkg.pr.new/@standard-server/peer@138

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@138

commit: 43b955d

@codecov

codecov Bot commented Sep 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/nodejs-adapter-rawbody-gcf-cc4a63 (43b955d) with main (917b6a2)

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@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.

ℹ️ Minor suggestions only — one behavioral edge case worth confirming.

Reviewed changes

  • Raw-body preference in the node adapter — toStandardBody now synthesizes a Readable from req.rawBody when it is a Uint8Array and the stream is already consumed, parsing those bytes with the normal hint logic instead of returning the framework's req.body.
  • Types and docs — adds rawBody?: unknown to NodeHttpRequest and documents the precedence in packages/node/README.md.
  • GCF integration test — new tests/google-cloud-functions.test.ts drives all six body kinds through the real Functions Framework test server; unit tests cover raw-body precedence and the non-Uint8Array fallback.
  • Dependency — adds @google-cloud/functions-framework as a root dev dependency.

I reproduced the regression coverage locally: against main's packages/node/src/body.ts, 5 of the 6 Functions Framework cases fail (json passes as the control) and the new precedence unit test fails; with the PR applied, packages/node plus the root tests suite (691 tests) pass.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Comment thread packages/node/src/body.ts
req: NodeHttpRequest,
options: ToStandardBodyOptions = {},
): Promise<StandardBody> {
const stream = !req.readable && req.rawBody instanceof Uint8Array ? Readable.from([req.rawBody]) : req

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.

For an empty application/json request, the Functions Framework leaves req.body as {} while req.rawBody is a zero-length Buffer. Because rawBody is now preferred, _streamToString yields '' and parseEmptyableJSON('') returns undefined, so GCP handlers that used to receive {} now receive undefined. This edge is not covered by the PR's behavior-change notes.

Technical details
# Empty JSON body on GCF resolves to `undefined` instead of `{}`

## Affected sites
- `packages/node/src/body.ts:24` — `req.rawBody instanceof Uint8Array` is true for the framework's zero-length `Buffer`, so the raw path is taken.
- `packages/shared/src/json.ts:1` — `parseEmptyableJSON('')` returns `undefined` (by design).

## Evidence
- Probed through the real Functions Framework with `POST`, `content-type: application/json`, empty body: `req.body` is `{}`, `req.rawBody` is a zero-length `Buffer`, and `toStandardBody(req)` returns `undefined`.
- Before the change the same request returned `{}` via the `req.body !== undefined && !req.readable` branch.

## Required outcome
- Confirm whether this normalization is intended. If so, add it to the PR/README behavior-change notes; the new value matches the non-GCF path (`parseEmptyableJSON('')`), so it may be the desired behavior.

## Open questions for the human
- Preserve `{}` for an empty parsed body, or standardize on `undefined` across all adapters?

@dinwwwh dinwwwh closed this Sep 30, 2026
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