Skip to content

feat(openapi)!: no default fallback for error response body options - #2350

Open
dinwwwh wants to merge 3 commits into
mainfrom
claude/compassionate-mccarthy-1m5yy0
Open

dinwwwh wants to merge 3 commits into
mainfrom
claude/compassionate-mccarthy-1m5yy0

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Returning null or undefined from errorResponseBodyEncoder (handler), errorResponseBodyDecoder (link) or errorResponseBodySchema (generator) no longer falls back to the default behavior. Each option now always decides, and the default stays reachable from its arguments: error.toJSON(), defaultError() and defaultSchema().

Changes

OpenAPIHandler — errorResponseBodyEncoder

  • The return value is always used as the response body. undefined now sends an empty error response, and null sends JSON null.
  • An empty body's content-type is left to the @standard-server adapters, which drop it as of feat(fetch, node)!: drop the content-type of an empty body standard-server#178 (not released yet; until then the configured errorResponseContentType is still sent with an empty body).

OpenAPILink — errorResponseBodyDecoder

  • Must return an Error, which the link throws as-is. () => null is now a type error.
  • Receives a lazy defaultError() as a third argument: the ORPC error decoded from the body (with its ResponseError cause), or a MalformedResponseError for other bodies.
  • The default decoding now returns that MalformedResponseError from decodeResponse instead of throwing it. StandardLink still throws it to the caller, so only the decode_response tracing span stops marking it as an error (the outer call span still records it).
  • StandardLinkCodecDecodedResponse (@orpc/client/standard) now accepts any Error instead of only ORPCError. StandardLink is the only consumer and just throws it.

OpenAPIGenerator — errorResponseBodySchema

  • null/undefined now documents an empty body: the response is emitted without content. This pairs with an encoder that returns undefined.
  • Receives a lazy defaultSchema() as a third argument. The per-code error components are only registered when it is called, so statuses with a custom or empty body no longer leave unused components behind.

Docs

  • The handler, link and specification examples return error.toJSON(), defaultError() and defaultSchema() respectively.
  • Noted the change in the v1 migration guide (section heading is now "Error response options changed").

Testing

  • pnpm vitest run (root suite)
  • pnpm type:check
  • pnpm lint
  • pnpm docs:validate

🤖 Generated with Claude Code

https://claude.ai/code/session_01VVaw7SJxePN5EdCgmjsAN5

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
orpc 3db1047 Commit Preview URL

Branch Preview URL
Oct 31 2026, 04:11 AM

@pkg-pr-new

pkg-pr-new Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

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

@orpc/arktype

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

@orpc/bun

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

@orpc/client

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

@orpc/cloudflare

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

@orpc/contract

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

@orpc/effect

npm i https://pkg.pr.new/@orpc/effect@2350

@orpc/evlog

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

@orpc/hibernation

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

@orpc/json-schema

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

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2350

@orpc/mcp

npm i https://pkg.pr.new/@orpc/mcp@2350

@orpc/msw

npm i https://pkg.pr.new/@orpc/msw@2350

@orpc/nest

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

@orpc/next

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

@orpc/node

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

@orpc/openapi

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

@orpc/opentelemetry

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

@orpc/pinia-colada

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

@orpc/pino

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

@orpc/publisher

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

@orpc/ratelimit

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

@orpc/server

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

@orpc/shared

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

@orpc/swr

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

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/zod

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

commit: 3db1047

@codecov

codecov Bot commented Oct 28, 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 Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

1 warning

⚠️ 5 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 28 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/compassionate-mccarthy-1m5yy0 (3db1047) with main (cf05b51)

Open in CodSpeed

Footnotes

  1. 2 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.

✅ No new issues found.

Reviewed changes — the OpenAPI error-response codec hooks lose their null/undefined fallback, plus the corresponding type, test, and docs updates:

  • Handler encoder always authoritative — errorResponseBodyEncoder's return value is always used as the body (undefined → empty body, null → JSON null); errorResponseContentType is now set only when the serialized body is not undefined. The default path still serializes error.toJSON().
  • Link decoder replaces decoding — errorResponseBodyDecoder is now required to return an Error (type narrowed from AnyORPCError | null | undefined) and its result is thrown as-is; the default ORPC-json decoding runs only when no decoder is configured.
  • Widened public type — StandardLinkCodecDecodedResponse's error arm widened from AnyORPCError to Error; the sole consumer just throws it, so the change is safe.
  • Docs & JSDoc — handler/link custom-error sections rewritten, migration guide updated, and @see backlinks added to isORPCErrorJson / createORPCErrorFromJson.

I traced the widened Error type through StandardLink (throws it) and confirmed the header omission matches the wire behavior — toFetchBody emits no content-type for an undefined body. The untouched generator errorResponseBodySchema fallback and its specification.mdx example are still accurate. Ran the three touched test files locally: 114 passed.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh dinwwwh changed the title feat(openapi)!: no default fallback for error response body encoder/decoder feat(openapi)!: no default fallback for error response body options Oct 28, 2026

@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 delta since the prior pullfrog review (commit 8e082c0), which extends the breaking change to the generator's errorResponseBodySchema:

  • Generator schema now always decides — errorResponseBodySchema must return a JsonSchema (type narrowed from JsonSchema | undefined | null) and receives a lazy defaultSchema() as a third argument. Returning false (a valid boolean schema, matches no value) omits content from the response object, documenting an empty body.
  • Lazy error-component registration — per-code error components are now registered only when defaultSchema() is actually invoked, so statuses served by a custom schema no longer leave unused components behind. UndefinedError stays eagerly registered whenever errors exist (pre-existing).
  • Docs & migration — specification.mdx's example takes defaultSchema and returns defaultSchema(); the migration guide's "Error response options changed" section now covers all three options (schema / encoder / decoder); tests cover defaultSchema(), the false empty-body case, and the registration optimization.

I traced the closure through buildErrorResponse, confirmed JsonSchema includes boolean (ensureJsonSchemaObject(schema: JsonSchema): Exclude<JsonSchema, boolean>), and verified the refactor preserves the previous eager hoistDefs behavior and ??-short-circuit semantics (registration is skipped exactly when the default is not used). Ran the four touched openapi test files locally: 162 passed; pnpm --filter @orpc/openapi type:check clean.

Pullfrog  | View workflow run | Using deepseek-v4.1-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 incremental delta since the prior pullfrog review (commit 8e082c0), a behavior-preserving cleanup on top of the breaking change:

  • Handler codec default resolved in constructor — errorResponseBodyEncoder now defaults to e => e.toJSON() in the constructor (field narrowed to Exclude<..., undefined>), so encodeError calls it unconditionally instead of the previous ?.(...) ?? error.toJSON() ternary. Behavior is unchanged.
  • Generator reuses hoisted defs — the lazy defaultSchema() now maps the already-hoisted definedErrors rather than re-running ctx.registry.hoistDefs on each data schema. hoistDefs reuses equivalent components, so the emitted document is identical; this drops a redundant second hoist that previously ran on the default path.
  • Docs & tests — the link decoder JSDoc now says "Throw a MalformedResponseError", matching the updated example which throws it; prose tightened across the four docs files; the duplicate link-codec decoder tests were merged into one.

I traced both refactors as behavior-preserving (hoistDefs idempotence confirmed via openapi-generator-components.ts:51 and its tests), ran the four touched openapi test files (202 passed), and pnpm --filter @orpc/openapi type:check is clean.

Pullfrog  | View workflow run | Using deepseek-v4.1-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 incremental delta since the prior pullfrog review (commit ce62fd6), which switches the generator's empty-error-body sentinel from false to null | undefined:

  • Sentinel switched to null | undefined — errorResponseBodySchema's return type is widened back from JsonSchema to JsonSchema | null | undefined; buildErrorResponse now omits content when the callback returns null or undefined (previously false), so a boolean false is once again a real schema that emits content.
  • Docs, JSDoc & tests updated — specification.mdx, the migration guide, and the generator JSDoc all describe null/undefined as the empty-body signal; the generator test covers both via it.each([null, undefined]), and the override test's default branch now returns defaultSchema().

I traced the new branch in buildErrorResponse (openapi-generator-operation.ts:550-558), confirmed the widened type still flows correctly into toOpenAPISchema, ran the two touched generator test files (57 passed), plus pnpm --filter @orpc/openapi type:check and eslint on the changed files — all clean.

Pullfrog  | View workflow run | Using deepseek-v4.1-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 critical issues — one minor docs fix inline.

Reviewed changes — the incremental delta since the prior pullfrog review (commit 663ec27), which adds a lazy defaultError() third argument to the OpenAPI link's errorResponseBodyDecoder:

  • defaultError() third argument — errorResponseBodyDecoder now receives a lazy defaultError: () => Error that returns the ORPC error (with its ResponseError cause) for ORPC bodies, or a MalformedResponseError otherwise. The option defaults to (_body, _response, defaultError) => defaultError(), and its field is narrowed to Exclude<..., undefined>, so the old if (...) guard is gone.
  • Default decoding returns instead of throws — decodeResponse builds defaultError and returns { kind: 'error', error: this.errorResponseBodyDecoder(deserialized, response, defaultError) }. Unknown payloads therefore surface as a returned MalformedResponseError rather than a throw; StandardLink still rethrows it to the caller, so only the decode_response tracing span stops marking it as an error (the outer call span still records it, as the PR notes).
  • JSDoc & docs — the decoder JSDoc trades the isORPCErrorJson/createORPCErrorFromJson/throw guidance for a @remarks bullet plus @default; the @see backlinks are removed from isORPCErrorJson/createORPCErrorFromJson (consistent, since those symbols are no longer imported in any docs code fence); the migration guide and link.mdx example now call defaultError().
  • Tests — a new case asserts defaultError() reproduces the default ORPC decoding and returns MalformedResponseError for unknown bodies; the unknown-payload test now reads the returned result.error.

I traced defaultError through decodeResponse and confirmed the throw-to-return change is not observable to callers (StandardLink.call throws the returned error at packages/client/src/adapters/standard/link.ts:121-123). Ran the two codec test files (106 passed) and the JSDoc/docs backlink checker (0 errors).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread apps/content/docs/openapi/link.mdx Resolved
Returning `null` or `undefined` from `errorResponseBodyEncoder`,
`errorResponseBodyDecoder` or `errorResponseBodySchema` no longer falls
back to the default behavior. Each option always decides, and the default
stays reachable from its arguments.

- Handler: the encoder's return value is always the body (`undefined`
  sends an empty one, without the configured `errorResponseContentType`);
  it defaults to `error.toJSON()`.
- Link: the decoder must return an `Error` and receives a lazy
  `defaultError()` that returns the decoded ORPC error or a
  `MalformedResponseError`. `StandardLinkCodecDecodedResponse` now accepts
  any `Error`.
- Generator: `errorResponseBodySchema` receives a lazy `defaultSchema()`
  (per-code error components are only registered when it is called), and
  `null`/`undefined` documents an empty body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SJxePN5EdCgmjsAN5
@dinwwwh
dinwwwh force-pushed the claude/compassionate-mccarthy-1m5yy0 branch from 7ad49f5 to 06ef511 Compare October 28, 2026 07:26
@dinwwwh
dinwwwh requested a balanced review from Copilot October 28, 2026 07:35

Copilot AI 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.

🟢 Approval recommended

No active review issues require changes.

No open issues 🎉

What changed in this PR

This PR makes OpenAPI error-response customization explicit across oRPC’s handler, client link, and specification generator. Custom callbacks no longer trigger default behavior by returning null or undefined.

Changes:

  • Uses handler encoder results directly, omitting the configured content type for empty bodies.
  • Adds explicit default helpers for decoding and schema generation, supports arbitrary decoded errors, and lazily registers per-code error schemas.
  • Updates tests, documentation examples, and migration guidance for the breaking behavior change.
File Description
packages/​openapi/​src/​openapi-generator.ts Updates schema callback types and documentation.
packages/​openapi/​src/​openapi-generator-operation.ts Adds lazy default schemas and content-free error responses.
packages/​openapi/​src/​openapi-generator-operation.test.ts Tests explicit defaults, lazy registration, and empty bodies.
packages/​openapi/​src/​adapters/​standard/​openapi-link-codec.ts Adds defaultError() and returns decoded errors directly.
packages/​openapi/​src/​adapters/​standard/​openapi-link-codec.test.ts Updates custom and malformed-response decoding tests.
packages/​openapi/​src/​adapters/​standard/​openapi-handler-codec.ts Removes implicit body fallback and handles empty-body headers.
packages/​openapi/​src/​adapters/​standard/​openapi-handler-codec.test.ts Tests null encoding and empty responses without content type.
packages/​client/​src/​adapters/​standard/​codec.ts Allows any Error in decoded error responses.
apps/​content/​docs/​openapi/​specification.mdx Uses defaultSchema() in the customization example.
apps/​content/​docs/​openapi/​link.mdx Uses defaultError() in the decoding example.
apps/​content/​docs/​openapi/​handler.mdx Uses error.toJSON() to preserve the default body.
apps/​content/​docs/​migrations/​from-v1.mdx Explains the removal of implicit defaults.

Review effort: Balanced

`@standard-server` adapters drop the `content-type` of an `undefined`
body (middleapi/standard-server#178), so the handler codec no longer
special-cases it. Also import `ORPCError` in the link decoder docs example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SJxePN5EdCgmjsAN5

@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 delta since the prior pullfrog review (06ef511 squashed head + ef1eab1 follow-up), a small cleanup on top of the breaking change:

  • Handler codec defers empty-body content-type to the adapter — encodeError drops the body !== undefined guard on the content-type header, so a configured errorResponseContentType is now always set. @standard-server adapters are expected to drop the header for an undefined body as of middleapi/standard-server#178; until that ships, an empty error body carries the configured content-type (called out in the PR body).
  • Docs fix — link.mdx's decoder example now imports ORPCError from @orpc/client, resolving the prior review's inline finding.
  • Test update — the empty-body encoder test drops errorResponseContentType from its config and is renamed to reflect that no fallback occurs.

I confirmed the deferral is a real (if low-stakes and documented) behavior change rather than a no-op: the currently installed @standard-server/fetch@0.13.0 does not clear content-type for an undefined body, so the header is sent until the ~0.13.0 range resolves to the #178 release. The prior inline finding is addressed, and no other concerns remain. Ran openapi-handler-codec.test.ts + openapi-link-codec.test.ts: 106 passed.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

No deployments
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.

3 participants