feat: add getPayload and getValidatedPayload utilities#1339
feat: add getPayload and getValidatedPayload utilities#1339productdevbook wants to merge 2 commits intomainfrom
Conversation
Merges route params, query params, and body into a single object:
- GET/HEAD: route params + query params
- POST/PUT/PATCH/DELETE: route params + parsed body
Route params have lowest priority — body/query values override them.
Usage:
app.post("/users/:id", async (event) => {
const payload = await getPayload(event);
// { id: "123", name: "Alice" }
});
Closes #785
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add getValidatedPayload(event, validate, options?) following the same pattern as getValidatedQuery and readValidatedBody. Validates the merged payload (route params + query/body) against a Standard Schema or custom validator function. As requested in the original issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR adds Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
test/unit/payload.test.ts (1)
7-85: Add explicit HEAD and DELETE cases to lock method-branch behavior.Current coverage validates GET/POST/PUT paths well, but dedicated tests for HEAD (query branch) and DELETE (body branch) would better protect the new method dispatch logic.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unit/payload.test.ts` around lines 7 - 85, Add two tests to explicitly cover HEAD (use getPayload) and DELETE (use getPayload/getValidatedPayload as appropriate) to lock method-branch behavior: create a HEAD route (e.g., t.app.head("/search", async (event) => getPayload(event))) and fetch "/search?q=test" with method "HEAD", asserting the returned payload contains the query param; create a DELETE route (e.g., t.app.delete("/items/:id", async (event) => getPayload(event))) and fetch "/items/123" with method "DELETE" and a JSON body, asserting the returned payload includes the body fields (and that body overrides route params on conflict), and similarly add a DELETE test for getValidatedPayload that returns 400 on invalid body to mirror the POST validation test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/utils/payload.ts`:
- Around line 38-39: The current return uses (await readBody(event)) || {} which
drops valid falsy parsed bodies (0, false, ""), so change the check to only
default when body is strictly undefined (e.g., const body = await
readBody(event); const safeBody = body === undefined ? {} : body) and then merge
using params and when safeBody is an object (typeof safeBody === "object" &&
safeBody !== null) spread it, otherwise include it as { body: safeBody }; update
references to readBody, body, params and the return expression accordingly.
- Around line 61-67: The custom-validator overloads (getValidatedPayload,
readValidatedBody, getValidatedQuery, getValidatedRouterParams) declare
options.onError as a zero-arg callback but the implementation calls it with a
FailureResult; update those overload signatures to use the OnValidateError type
(i.e., options?: { onError?: OnValidateError }) to match the StandardSchema
overloads and the actual call site. Ensure the onError param type accepts the
FailureResult (or the existing FailureResult type alias) so callers can inspect
validation. Adjust any related type imports/exports (OnValidateError,
FailureResult) so the overloads and implementation are consistent for
ValidateResult/ValidateResult<OutputT> usage.
---
Nitpick comments:
In `@test/unit/payload.test.ts`:
- Around line 7-85: Add two tests to explicitly cover HEAD (use getPayload) and
DELETE (use getPayload/getValidatedPayload as appropriate) to lock method-branch
behavior: create a HEAD route (e.g., t.app.head("/search", async (event) =>
getPayload(event))) and fetch "/search?q=test" with method "HEAD", asserting the
returned payload contains the query param; create a DELETE route (e.g.,
t.app.delete("/items/:id", async (event) => getPayload(event))) and fetch
"/items/123" with method "DELETE" and a JSON body, asserting the returned
payload includes the body fields (and that body overrides route params on
conflict), and similarly add a DELETE test for getValidatedPayload that returns
400 on invalid body to mirror the POST validation test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9eb2d4d5-4484-45e2-8767-9e266494afe2
📒 Files selected for processing (4)
src/index.tssrc/utils/payload.tstest/unit/package.test.tstest/unit/payload.test.ts
| const body = (await readBody(event)) || {}; | ||
| return { ...params, ...(typeof body === "object" ? body : { body }) } as T; |
There was a problem hiding this comment.
Preserve falsy JSON primitive bodies instead of dropping them.
Line 38 uses || {}, so valid parsed bodies like 0, false, and "" are treated as empty and lost.
Suggested fix
- const body = (await readBody(event)) || {};
- return { ...params, ...(typeof body === "object" ? body : { body }) } as T;
+ const body = await readBody(event);
+ return {
+ ...params,
+ ...(body === undefined
+ ? {}
+ : body !== null && typeof body === "object"
+ ? body
+ : { body }),
+ } as T;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/payload.ts` around lines 38 - 39, The current return uses (await
readBody(event)) || {} which drops valid falsy parsed bodies (0, false, ""), so
change the check to only default when body is strictly undefined (e.g., const
body = await readBody(event); const safeBody = body === undefined ? {} : body)
and then merge using params and when safeBody is an object (typeof safeBody ===
"object" && safeBody !== null) spread it, otherwise include it as { body:
safeBody }; update references to readBody, body, params and the return
expression accordingly.
| export function getValidatedPayload<Event extends HTTPEvent, OutputT>( | ||
| event: Event, | ||
| validate: ( | ||
| data: Record<string, unknown>, | ||
| ) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>, | ||
| options?: { onError?: () => ErrorDetails }, | ||
| ): Promise<OutputT>; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify onError callback signatures across validated helpers.
rg -nP --type=ts -C3 'export function getValidated(Payload|Query|Body)\(' src/utils
rg -nP --type=ts -C3 'onError\?:' src/utils/payload.ts src/utils/request.ts src/utils/body.ts src/utils/internal/validate.tsRepository: h3js/h3
Length of output: 7268
🏁 Script executed:
rg -nP --type=ts 'type OnValidateError' src/
rg -nP --type=ts -A5 'onError\(' src/utils/internal/validate.tsRepository: h3js/h3
Length of output: 1654
🏁 Script executed:
sed -n '10,20p' src/utils/internal/validate.tsRepository: h3js/h3
Length of output: 360
Custom-validator overloads have inconsistent onError signatures across multiple functions.
The custom validator overloads narrow onError to a zero-arg callback, but the implementation always invokes it with a FailureResult object. This typing mismatch breaks type inference for accessing validation issues.
Affected overloads:
getValidatedPayload(line 66)readValidatedBody(line 62)getValidatedQuery(line 70)getValidatedRouterParams(line 174)
Change custom validator overloads to match OnValidateError type and StandardSchema counterparts:
Example fix for getValidatedPayload
export function getValidatedPayload<Event extends HTTPEvent, OutputT>(
event: Event,
validate: (
data: Record<string, unknown>,
) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>,
- options?: { onError?: () => ErrorDetails },
+ options?: { onError?: (result: FailureResult) => ErrorDetails },
): Promise<OutputT>;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function getValidatedPayload<Event extends HTTPEvent, OutputT>( | |
| event: Event, | |
| validate: ( | |
| data: Record<string, unknown>, | |
| ) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>, | |
| options?: { onError?: () => ErrorDetails }, | |
| ): Promise<OutputT>; | |
| export function getValidatedPayload<Event extends HTTPEvent, OutputT>( | |
| event: Event, | |
| validate: ( | |
| data: Record<string, unknown>, | |
| ) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>, | |
| options?: { onError?: (result: FailureResult) => ErrorDetails }, | |
| ): Promise<OutputT>; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/payload.ts` around lines 61 - 67, The custom-validator overloads
(getValidatedPayload, readValidatedBody, getValidatedQuery,
getValidatedRouterParams) declare options.onError as a zero-arg callback but the
implementation calls it with a FailureResult; update those overload signatures
to use the OnValidateError type (i.e., options?: { onError?: OnValidateError })
to match the StandardSchema overloads and the actual call site. Ensure the
onError param type accepts the FailureResult (or the existing FailureResult type
alias) so callers can inspect validation. Adjust any related type
imports/exports (OnValidateError, FailureResult) so the overloads and
implementation are consistent for ValidateResult/ValidateResult<OutputT> usage.
| const params = getRouterParams(event, opts); | ||
| if (_payloadMethods.has(event.req.method)) { | ||
| const body = (await readBody(event)) || {}; | ||
| return { ...params, ...(typeof body === "object" ? body : { body }) } as T; |
| const body = (await readBody(event)) || {}; | ||
| return { ...params, ...(typeof body === "object" ? body : { body }) } as T; | ||
| } | ||
| const query = getQuery(event); |
There was a problem hiding this comment.
We can use event.url to get query instead of getQuery
| event: H3Event | HTTPEvent, | ||
| opts?: { decode?: boolean }, | ||
| ): Promise<T> { | ||
| const params = getRouterParams(event, opts); |
There was a problem hiding this comment.
router params should not be included. only query+body
There was a problem hiding this comment.
in my original issue #785, I did include route params. This way has been most useful for me. Any downsides?
| ): Promise<T> { | ||
| const params = getRouterParams(event, opts); | ||
| if (_payloadMethods.has(event.req.method)) { | ||
| const body = (await readBody(event)) || {}; |
danielkellyio
left a comment
There was a problem hiding this comment.
just a couple comments based on my usage of a similar project specific utility. Thanks!
| event: H3Event | HTTPEvent, | ||
| opts?: { decode?: boolean }, | ||
| ): Promise<T> { | ||
| const params = getRouterParams(event, opts); |
There was a problem hiding this comment.
in my original issue #785, I did include route params. This way has been most useful for me. Any downsides?
| * }); | ||
| * | ||
| * @example | ||
| * app.get("/search/:category", async (event) => { |
There was a problem hiding this comment.
It's also been useful in my experience to include query params for post requests, so that order of importance goes:
- body
- query params
- route params
For example a post request to:
/users/:id?email=test@test.com
with the body:
{ name: "Alice" }
would result in:
const { id, name, email } = await getPayload(event);
Why?
Mostly just when I'm moving fast and mix up query and body. But the idea being the payload is just everything that we pass as data to the backend
Summary
Closes #785
Adds
getPayloadandgetValidatedPayloadutilities that merge route params, query params, and body into a single object — depending on the request method.As pi0 noted: "Idea is that a utility to work with both query/body."
getPayload(event)getValidatedPayload(event, schema)Behavior
Route params have lowest priority — body/query values override them on conflict.
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Tests