Skip to content

feat(openapi): configurable media type for error responses - #2337

Open
dinwwwh wants to merge 2 commits into
mainfrom
claude/pensive-curie-usfpum
Open

dinwwwh wants to merge 2 commits into
mainfrom
claude/pensive-curie-usfpum

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 25, 2026

Copy link
Copy Markdown
Member

Closes #2317

Adds an errorResponseContentType option to OpenAPIHandler, OpenAPILink and OpenAPIGenerator.generate(). Use it to send and document errors under a different media type, such as application/problem+json for RFC 9457 Problem Details.

const handler = new OpenAPIHandler(router, {
  customErrorResponseBodyEncoder: toProblemDetails,
  errorResponseContentType: 'application/problem+json',
})

const link = new OpenAPILink(contract, {
  errorResponseContentType: 'application/problem+json',
})

const spec = await generator.generate(router, {
  customErrorResponseBodySchema: problemDetailsJsonSchema,
  errorResponseContentType: 'application/problem+json',
})

Changes

  • Handler: when the option is set, encodeError sends the JSON error body as a Blob of that type. @standard-server always sends plain JSON bodies as application/json, but it keeps a Blob's own type, so no @standard-server change is needed. Because of this, these responses also carry standard-server: file, content-disposition: inline; filename="blob" and content-length headers.
  • Link: an error response whose content-type equals the option is read with resolveBody('json'). All other responses still use responseBodyHint. A non-JSON error page from a proxy therefore still gives a MalformedResponseError.
  • Generator: error responses are documented under the configured type instead of the hardcoded application/json.
  • Docs: the handler, link and specification pages describe the option.

Without the option, nothing changes.

Tests

  • Codec unit tests for the handler (Blob body and its type) and for the link (forces 'json' only on error responses with a matching type).
  • Generator unit test and end-to-end test for the documented media type.
  • A round trip through the fetch OpenAPIHandler and OpenAPILink: the response is sent as application/problem+json, and the client throws the matching ORPCError.
  • pnpm vitest run packages/openapi packages/nest, tsc, @orpc/nest type check, lint and pnpm docs:validate all pass.

Generated by Claude Code

Add `errorResponseContentType` to `OpenAPIHandler` and
`OpenAPIGenerator.generate()`, e.g. `application/problem+json` for
RFC 9457 Problem Details.

The generator documents error responses under the configured media
type. The handler codec sets the `content-type` header on error
responses; it reaches the wire once `@standard-server` preserves a
provided content type for JSON bodies.

Refs #2317

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

With `errorResponseContentType` set, the handler sends the error body as a
JSON Blob of that type, since standard-server always sends plain JSON bodies
as `application/json`. `OpenAPILink` gets the matching option and parses error
responses with that content type as JSON.

Refs #2317

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JepnhdVCkPJwexoiHyqRR
@cloudflare-workers-and-pages

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 61bf41d Commit Preview URL

Branch Preview URL
Oct 25 2026, 03:56 AM

@codecov

codecov Bot commented Oct 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pkg-pr-new

pkg-pr-new Bot commented Oct 25, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

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

@orpc/arktype

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

@orpc/bun

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

@orpc/client

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

@orpc/cloudflare

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

@orpc/contract

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

@orpc/effect

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

@orpc/evlog

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

@orpc/hibernation

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

@orpc/json-schema

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

@orpc/experimental-lock

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

@orpc/mcp

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

@orpc/msw

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

@orpc/nest

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

@orpc/next

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

@orpc/node

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

@orpc/openapi

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

@orpc/opentelemetry

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

@orpc/pinia-colada

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

@orpc/pino

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

@orpc/publisher

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

@orpc/ratelimit

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

@orpc/server

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

@orpc/shared

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

@orpc/swr

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

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/zod

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

commit: 61bf41d

@codspeed

codspeed Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

1 warning

⚠️ 6 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/pensive-curie-usfpum (61bf41d) with main (00bfdba)

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.

ℹ️ One minor interoperability suggestion inline — nothing blocking.

Reviewed changes

  • Handler encoding — encodeError wraps the serialized error body in a Blob typed as errorResponseContentType when set, relying on @standard-server preserving a Blob's own content type.
  • Link decoding — error responses whose content-type matches the configured option are parsed with resolveBody('json'); everything else keeps using responseBodyHint.
  • Generator — errorResponseContentType is threaded through OpenAPIOperationContext (default 'application/json') and applied only to buildErrorResponse, so success bodies are untouched.
  • Tests / docs — unit, e2e and round-trip coverage for the handler, link and generator, plus handler/link/specification docs.

Verified locally: pnpm vitest run packages/openapi packages/nest (608 tests) and pnpm type:check both pass. I also confirmed the node/aws-lambda/fastify adapters preserve the Blob type verbatim (Fastify returns application/problem+json with no ; charset=utf-8 appended), so the custom media type survives the built-in adapters.

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

const body = await response.resolveBody(meta?.responseBodyHint)
const isCustomErrorContentType = !isOk
&& this.errorResponseContentType !== undefined
&& flattenStandardHeader(response.headers['content-type']) === this.errorResponseContentType

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.

This compares the raw content-type header to the option with strict string equality, so a media type that carries parameters or different casing (application/problem+json; charset=utf-8, Application/Problem+JSON) won't match. Since OpenAPILink is documented to call any OpenAPI-compliant server, that mismatch would fall back to the header-derived hint — and because the handler sets standard-server: file, the body resolves as a File and decodeResponse throws MalformedResponseError on a valid JSON body.

Technical details
# Normalize the media type before comparing

## Affected sites
- `packages/openapi/src/adapters/standard/openapi-link-codec.ts:397` — `flattenStandardHeader(response.headers['content-type']) === this.errorResponseContentType`

## Required outcome
- A configured `errorResponseContentType` matches an error response whose `content-type` is the same media type, ignoring parameters and case (e.g. `application/problem+json` matches `application/problem+json; charset=utf-8`).

## Suggested approach
- Compare the normalized media type (split on `;`, trim, lowercase) rather than the raw header, mirroring `resolveStandardBodyHint` in `@standard-server/core`, which already does `?.split(';')[0]?.trim().toLowerCase()`.

## Open questions for the human
- Whether to be lenient here, given the handler and link are usually configured with identical strings in oRPC's own adapter stack.

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.

[v2] Media type for OpenAPI error responses

2 participants