Skip to content

feat(server,openapi): support ReadableStream<Uint8Array> as handler return value#1535

Merged
dinwwwh merged 9 commits intomiddleapi:mainfrom
ghdoergeloh:feat/readable-stream-handler-return
Apr 22, 2026
Merged

feat(server,openapi): support ReadableStream<Uint8Array> as handler return value#1535
dinwwwh merged 9 commits intomiddleapi:mainfrom
ghdoergeloh:feat/readable-stream-handler-return

Conversation

@ghdoergeloh
Copy link
Copy Markdown
Contributor

@ghdoergeloh ghdoergeloh commented Apr 20, 2026

Summary

Adds partial support for returning a ReadableStream<Uint8Array> directly from an oRPC handler (OpenAPIHandler only), bypassing JSON serialization. This enables streaming binary responses (ZIP archives, large file downloads, chunked transfer) without buffering the full response in memory.

Note: Full ecosystem support (RPCHandler/Link, OpenAPILink, WebSocket adapters, etc.) is planned for v2 — see #1058. This PR intentionally does not close #1058.

Stream passthrough

  • @orpc/standard-server — adds ReadableStream<Uint8Array> to the StandardBody union type
  • @orpc/serverStandardRPCCodec.encode() detects ReadableStream and passes it through without serializing
  • @orpc/openapiStandardOpenAPICodec.encode() detects ReadableStream (both as direct return and inside { body: stream, headers } detailed output), respects the contract's successStatus, and bypasses serialization
  • @orpc/standard-server-fetchtoFetchBody() returns the stream directly; the ReadableStream branch runs before header cleanup so caller-set content-type/content-disposition are preserved
  • @orpc/standard-server-nodetoNodeHttpBody() converts via Readable.fromWeb(); same header-preservation fix
  • @orpc/standard-servergenerateContentDisposition() gets an optional disposition parameter (defaults to 'inline', fully backward-compatible)

OpenAPI spec cleanup (fixes #1536)

  • @orpc/openapiseparateObjectSchema no longer leaves empty properties: {} / required: [] on extracted schemas (replaces them with undefined)
  • @orpc/openapi — new isNeverSchema() helper identifies false / { not: true } / { not: {} } schemas; toOpenAPIContent() now skips the spurious application/json: { schema: { not: {} } } entry that used to appear alongside binary content (e.g. when using oz.openapi(z.instanceof(ReadableStream), { contentMediaType: 'application/zip' }))
  • @orpc/openapi — generator skips emitting an empty requestBody for POST routes whose input consists only of path parameters

Tests added for all new paths including header preservation, detailed-output streaming, never-schema detection, and the empty-requestBody case.

Backward compatibility

This is a non-breaking change.

  • Previously, returning a ReadableStream from a handler would result in it being serialized as {} (or a runtime error), which is not useful behaviour anyone would rely on.
  • generateContentDisposition()'s new disposition parameter defaults to 'inline', preserving existing output.
  • The OpenAPI spec cleanups only suppress output that was previously noise (empty bodies, not: {} alongside real content).

Existing handlers and generated specs are unaffected.

Motivation

We are migrating a service from Hapi.js to Hono + oRPC. One endpoint streams a ZIP archive on-the-fly using archiver — the archive must be piped directly to the response. All existing workarounds (returning new Response(stream), oz.blob(), casting) either fail at runtime or require bypassing oRPC entirely, losing auth middleware and client-library support.

Relates to #1058 (partial — OpenAPIHandler only)
Closes #1536

Test plan

  • pnpm --filter @orpc/standard-server test
  • pnpm --filter @orpc/server test
  • pnpm --filter @orpc/openapi test
  • pnpm --filter @orpc/standard-server-fetch test
  • pnpm --filter @orpc/standard-server-node test

🤖 Generated with Claude Code

…eturn value

Allow oRPC handlers to return a ReadableStream<Uint8Array> directly,
bypassing JSON serialization. The stream is passed through the codec
layer unchanged and forwarded to the fetch/Node HTTP adapters.

Also adds an optional `disposition` parameter to `generateContentDisposition`
(defaults to `'inline'` for backward compatibility).

Fixes https://github.com/unnoq/orpc/issues/1058

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Apr 20, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds explicit support for Web ReadableStream<Uint8Array> across codecs, body adapters, types, utils, and tests: stream bodies bypass serializer and are passed through (or converted to Node Readable) with preserved status and headers where provided. No exported API signatures were widened except StandardBody type union and a utils parameter change. (50 words)

Changes

Cohort / File(s) Summary
Types & Utils
packages/standard-server/src/types.ts, packages/standard-server/src/utils.ts
Adds ReadableStream<Uint8Array> to StandardBody. generateContentDisposition gains optional `disposition: 'inline'
RPC Codec
packages/server/src/adapters/standard/rpc-codec.ts, packages/server/src/adapters/standard/rpc-codec.test.ts
StandardRPCCodec.encode short-circuits when output is a ReadableStream, returning { status: 200, headers: {}, body: stream }; test verifies serializer is not called.
OpenAPI Codec
packages/openapi/src/adapters/standard/openapi-codec.ts, packages/openapi/src/adapters/standard/openapi-codec.test.ts
StandardOpenAPICodec.encode returns stream directly when output or output.body is a ReadableStream, preserving status/headers and bypassing serializer; tests cover compact and detailed outputs.
Fetch Body Adapter
packages/standard-server-fetch/src/body.ts, packages/standard-server-fetch/src/body.test.ts
toFetchBody returns ReadableStream<Uint8Array> inputs unchanged, avoiding header/content-type/disposition mutation; test verifies passthrough and consumability.
Node Body Adapter
packages/standard-server-node/src/body.ts, packages/standard-server-node/src/body.test.ts
toNodeHttpBody detects body instanceof ReadableStream early and converts via Readable.fromWeb(body) to a Node Readable; test checks conversion, header preservation, and readable consumption.
OpenAPI utils & schema
packages/openapi/src/openapi-utils.ts, packages/openapi/src/openapi-utils.test.ts, packages/openapi/src/schema-utils.ts, packages/openapi/src/schema-utils.test.ts
toOpenAPIContent now avoids emitting application/json when the "rest" branch is an unconstrained/any schema; isAnySchema and separateObjectSchema behavior refined to ignore undefined keys and normalize empty properties/required; tests updated/added.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler
  participant Codec
  participant Serializer
  participant Adapter
  participant Runtime

  Client->>Handler: invoke handler()
  Handler-->>Client: returns ReadableStream<Uint8Array>

  Client->>Codec: encode(output)
  alt output is ReadableStream
    Codec-->>Client: StandardResponse(status, headers, stream)
  else non-stream output
    Codec->>Serializer: serialize(output)
    Serializer-->>Codec: serializedBody
    Codec-->>Client: StandardResponse(status, headers, serializedBody)
  end

  Client->>Adapter: toFetchBody / toNodeHttpBody(response.body)
  alt Web ReadableStream
    Adapter-->>Runtime: pass-through stream or Readable.fromWeb(stream)
  end

  Runtime-->>Client: HTTP response with stream body
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

size:XL

Poem

🐰 I nudged a stream into the night,
No stringify, no byte-array fight.
It flows through codec, fetch, and node,
A silky path along the road.
Hooray — sweet bytes in steady flight! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: adding ReadableStream support as a handler return value across server and OpenAPI packages.
Linked Issues check ✅ Passed The PR successfully implements the primary objective from issue #1058: allowing handlers to return ReadableStream for streaming binary responses without buffering, with comprehensive support across all server adapters and OpenAPI generation.
Out of Scope Changes check ✅ Passed Schema utility refinements (isAnySchema, separateObjectSchema) and OpenAPI content generation updates are tightly scoped to supporting the ReadableStream feature and represent necessary supporting changes, not out-of-scope additions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added enhancement New feature or request javascript Pull requests that update javascript code labels Apr 20, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for ReadableStream in the framework's adapters and server implementations, allowing binary data to be streamed directly. It updates the StandardBody type, modifies the fetch and node body converters, and enhances the generateContentDisposition utility. Feedback suggests that the StandardOpenAPICodec should be updated to respect contract-defined success statuses and support streams within detailed response objects to allow for custom headers and status codes.

Comment thread packages/openapi/src/adapters/standard/openapi-codec.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/standard-server-node/src/body.ts (1)

65-97: ⚠️ Potential issue | 🟠 Major

Preserve explicit stream response headers.

Line 95 returns the stream only after Lines 67-68 delete content-type and content-disposition, so a streamed ZIP response loses application/zip and attachment filename metadata. Move the stream branch before the generic header cleanup and only clear stale JSON defaults.

🐛 Proposed fix
 export function toNodeHttpBody(
   body: StandardBody,
   headers: StandardHeaders,
   options: ToNodeHttpBodyOptions = {},
 ): Readable | undefined | string {
+  if (body instanceof ReadableStream) {
+    if (flattenHeader(headers['content-type'])?.startsWith('application/json')) {
+      delete headers['content-type']
+    }
+
+    return Readable.fromWeb(body)
+  }
+
   const currentContentDisposition = flattenHeader(headers['content-disposition'])
 
   delete headers['content-type']
   delete headers['content-disposition']
@@
-  if (body instanceof ReadableStream) {
-    return Readable.fromWeb(body)
-  }
-
   if (isAsyncIteratorObject(body)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/standard-server-node/src/body.ts` around lines 65 - 97, The stream
branch currently runs after we unconditionally delete headers['content-type']
and headers['content-disposition'], which strips explicit stream response
headers (e.g., application/zip and attachment filename); move the "if (body
instanceof ReadableStream) return Readable.fromWeb(body)" branch to before the
header deletion so explicit response headers are preserved (refer to the
identifier body and Readable.fromWeb), and change the header cleanup around
lines that touch headers['content-type'] / headers['content-disposition'] to
only remove stale JSON defaults (do not remove existing
content-type/content-disposition when body is a stream or Blob/File—see
currentContentDisposition and generateContentDisposition usage).
packages/standard-server-fetch/src/body.ts (1)

70-100: ⚠️ Potential issue | 🟠 Major

Do not strip explicit headers for raw streams.

Line 98 returns the stream after Lines 72-73 remove content-type and content-disposition. That leaves binary stream responses without media type or filename metadata.

🐛 Proposed fix
 export function toFetchBody(
   body: StandardBody,
   headers: Headers,
   options: ToFetchBodyOptions = {},
 ): string | Blob | FormData | URLSearchParams | undefined | ReadableStream<Uint8Array> {
+  if (body instanceof ReadableStream) {
+    if (headers.get('content-type')?.startsWith('application/json')) {
+      headers.delete('content-type')
+    }
+
+    return body
+  }
+
   const currentContentDisposition = headers.get('content-disposition')
 
   headers.delete('content-type')
   headers.delete('content-disposition')
@@
-  if (body instanceof ReadableStream) {
-    return body
-  }
-
   if (isAsyncIteratorObject(body)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/standard-server-fetch/src/body.ts` around lines 70 - 100, The code
currently unconditionally deletes content-type and content-disposition
(headers.delete('content-type') / headers.delete('content-disposition')) before
branching on body types, which strips metadata for raw ReadableStream responses;
change the logic so headers are only removed for body types that will be re-set
(e.g., Blob, File, FormData, URLSearchParams) and do not delete these headers
when body is a ReadableStream — i.e., move or gate the header deletion behind
the non-stream branches (or early-return for ReadableStream) so ReadableStream
responses keep their existing content-type and content-disposition (reference
variables/functions: headers, body, currentContentDisposition, Blob, File,
ReadableStream, generateContentDisposition).
packages/openapi/src/adapters/standard/openapi-codec.ts (1)

75-112: ⚠️ Potential issue | 🟠 Major

Keep OpenAPI status/headers semantics for streamed bodies.

Line 75 bypasses the contract-derived success status and Line 111 still serializes output.body for detailed outputs. This makes streamed OpenAPI responses unable to use configured status codes or detailed { headers, body } metadata.

🐛 Proposed fix
   encode(output: unknown, procedure: AnyProcedure): StandardResponse {
+    const successStatus = fallbackContractConfig('defaultSuccessStatus', procedure['~orpc'].route.successStatus)
+    const outputStructure = fallbackContractConfig('defaultOutputStructure', procedure['~orpc'].route.outputStructure)
+
     if (output instanceof ReadableStream) {
       return {
-        status: 200,
+        status: successStatus,
         headers: {},
         body: output,
       }
     }
 
-    const successStatus = fallbackContractConfig('defaultSuccessStatus', procedure['~orpc'].route.successStatus)
-    const outputStructure = fallbackContractConfig('defaultOutputStructure', procedure['~orpc'].route.outputStructure)
-
     if (outputStructure === 'compact') {
       return {
         status: successStatus,
@@
     return {
       status: output.status ?? successStatus,
       headers: output.headers ?? {},
-      body: this.serializer.serialize(output.body),
+      body: output.body instanceof ReadableStream
+        ? output.body
+        : this.serializer.serialize(output.body),
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/openapi/src/adapters/standard/openapi-codec.ts` around lines 75 -
112, The current ReadableStream branch short-circuits and ignores
contract-derived status/headers and later always calls
this.serializer.serialize(output.body); update the logic so streamed bodies
retain OpenAPI status/headers semantics: use
fallbackContractConfig('defaultSuccessStatus',
procedure['~orpc'].route.successStatus) for compact streams (return {status:
successStatus, headers:{}, body: stream}) and treat detailed outputs that have a
stream in output.body as valid in `#isDetailedOutput` path but do not call
this.serializer.serialize on stream bodies—return the stream directly; ensure
`#isDetailedOutput` still validates objects with optional status/headers/body and
that headers default to {} and status defaults to successStatus when omitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/standard-server-fetch/src/body.test.ts`:
- Around line 280-298: The test currently expects all stream-related headers to
be stripped; update it to assert that non-JSON stream headers are preserved by
toFetchBody: create the ReadableStream and pass headers that include
'content-type': 'application/zip' and 'content-disposition': 'attachment;
filename="foo.zip"' alongside the existing custom header, call
toFetchBody(stream, headers, {}), assert the returned body is the original
stream, then assert that the headers iterable still contains x-custom-header
plus content-type and content-disposition entries, and finally verify the stream
can be read (e.g., via new Response(body).arrayBuffer() or .text()) to ensure
conversion didn't consume/alter the stream. Ensure you reference toFetchBody and
the test case name (readable stream) when making the change.

In `@packages/standard-server-node/src/body.test.ts`:
- Around line 408-426: Update the "readable stream" test to include and assert
preservation of media metadata: add 'content-type' and 'content-disposition'
into the headers (e.g. extend baseHeaders before calling toNodeHttpBody) and
after calling toNodeHttpBody assert those header keys and their values are still
present (alongside 'x-custom-header'); reference the test symbols headers,
baseHeaders and the toNodeHttpBody call so the test ensures content-type and
content-disposition are preserved for raw stream payloads.

---

Outside diff comments:
In `@packages/openapi/src/adapters/standard/openapi-codec.ts`:
- Around line 75-112: The current ReadableStream branch short-circuits and
ignores contract-derived status/headers and later always calls
this.serializer.serialize(output.body); update the logic so streamed bodies
retain OpenAPI status/headers semantics: use
fallbackContractConfig('defaultSuccessStatus',
procedure['~orpc'].route.successStatus) for compact streams (return {status:
successStatus, headers:{}, body: stream}) and treat detailed outputs that have a
stream in output.body as valid in `#isDetailedOutput` path but do not call
this.serializer.serialize on stream bodies—return the stream directly; ensure
`#isDetailedOutput` still validates objects with optional status/headers/body and
that headers default to {} and status defaults to successStatus when omitted.

In `@packages/standard-server-fetch/src/body.ts`:
- Around line 70-100: The code currently unconditionally deletes content-type
and content-disposition (headers.delete('content-type') /
headers.delete('content-disposition')) before branching on body types, which
strips metadata for raw ReadableStream responses; change the logic so headers
are only removed for body types that will be re-set (e.g., Blob, File, FormData,
URLSearchParams) and do not delete these headers when body is a ReadableStream —
i.e., move or gate the header deletion behind the non-stream branches (or
early-return for ReadableStream) so ReadableStream responses keep their existing
content-type and content-disposition (reference variables/functions: headers,
body, currentContentDisposition, Blob, File, ReadableStream,
generateContentDisposition).

In `@packages/standard-server-node/src/body.ts`:
- Around line 65-97: The stream branch currently runs after we unconditionally
delete headers['content-type'] and headers['content-disposition'], which strips
explicit stream response headers (e.g., application/zip and attachment
filename); move the "if (body instanceof ReadableStream) return
Readable.fromWeb(body)" branch to before the header deletion so explicit
response headers are preserved (refer to the identifier body and
Readable.fromWeb), and change the header cleanup around lines that touch
headers['content-type'] / headers['content-disposition'] to only remove stale
JSON defaults (do not remove existing content-type/content-disposition when body
is a stream or Blob/File—see currentContentDisposition and
generateContentDisposition usage).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e3c9da6-a1c8-4975-8416-3f7ed5eca230

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee06ff and 01ec98c.

📒 Files selected for processing (9)
  • packages/openapi/src/adapters/standard/openapi-codec.ts
  • packages/server/src/adapters/standard/rpc-codec.test.ts
  • packages/server/src/adapters/standard/rpc-codec.ts
  • packages/standard-server-fetch/src/body.test.ts
  • packages/standard-server-fetch/src/body.ts
  • packages/standard-server-node/src/body.test.ts
  • packages/standard-server-node/src/body.ts
  • packages/standard-server/src/types.ts
  • packages/standard-server/src/utils.ts

Comment thread packages/standard-server-fetch/src/body.test.ts
Comment thread packages/standard-server-node/src/body.test.ts
@dinwwwh
Copy link
Copy Markdown
Member

dinwwwh commented Apr 20, 2026

Happy to merge this to partially support ReadableStream in OpenAPIHandler (only)

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 20, 2026

More templates

@orpc/ai-sdk

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

@orpc/arktype

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

@orpc/client

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

@orpc/contract

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

@orpc/experimental-durable-iterator

npm i https://pkg.pr.new/@orpc/experimental-durable-iterator@1535

@orpc/hey-api

npm i https://pkg.pr.new/@orpc/hey-api@1535

@orpc/interop

npm i https://pkg.pr.new/@orpc/interop@1535

@orpc/json-schema

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

@orpc/nest

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

@orpc/openapi

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

@orpc/openapi-client

npm i https://pkg.pr.new/@orpc/openapi-client@1535

@orpc/otel

npm i https://pkg.pr.new/@orpc/otel@1535

@orpc/experimental-pino

npm i https://pkg.pr.new/@orpc/experimental-pino@1535

@orpc/experimental-publisher

npm i https://pkg.pr.new/@orpc/experimental-publisher@1535

@orpc/experimental-publisher-durable-object

npm i https://pkg.pr.new/@orpc/experimental-publisher-durable-object@1535

@orpc/experimental-ratelimit

npm i https://pkg.pr.new/@orpc/experimental-ratelimit@1535

@orpc/react

npm i https://pkg.pr.new/@orpc/react@1535

@orpc/react-query

npm i https://pkg.pr.new/@orpc/react-query@1535

@orpc/experimental-react-swr

npm i https://pkg.pr.new/@orpc/experimental-react-swr@1535

@orpc/server

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

@orpc/shared

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

@orpc/solid-query

npm i https://pkg.pr.new/@orpc/solid-query@1535

@orpc/standard-server

npm i https://pkg.pr.new/@orpc/standard-server@1535

@orpc/standard-server-aws-lambda

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

@orpc/standard-server-fastify

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

@orpc/standard-server-fetch

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

@orpc/standard-server-node

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

@orpc/standard-server-peer

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

@orpc/svelte-query

npm i https://pkg.pr.new/@orpc/svelte-query@1535

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/vue-colada

npm i https://pkg.pr.new/@orpc/vue-colada@1535

@orpc/vue-query

npm i https://pkg.pr.new/@orpc/vue-query@1535

@orpc/zod

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

commit: 8235e5c

…support detailed stream output

- Move ReadableStream branch before header deletion in toFetchBody and
  toNodeHttpBody so caller-set content-type/content-disposition are preserved
- In StandardOpenAPICodec, read successStatus before the ReadableStream check
  so contract-configured status codes are respected
- Support ReadableStream inside detailed output ({ body: stream, headers })
  in StandardOpenAPICodec, allowing custom headers alongside streaming responses
- Update tests to assert content-type/content-disposition preservation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Apr 20, 2026
…a branches are empty

When a schema union has file branches (e.g. contentMediaType: application/zip)
and all remaining branches resolve to nothing (empty anyOf/oneOf), the generator
was emitting a spurious 'application/json: { schema: { anyOf: [] } }' entry.
Skip the application/json content entry when the rest schema is empty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/openapi/src/openapi-utils.ts`:
- Around line 36-42: The current isEmptyRest flag incorrectly treats a boolean
false schema as empty (because of "restSchema === false"), causing
toOpenAPIContent(false) to return {} and drop "{ not: {} }"; change the logic so
only object schemas with extracted file-branch unions are considered empty:
remove the "restSchema === false" clause and compute isEmptyRest only when
isObject(restSchema) && ((Array.isArray(restSchema.anyOf) &&
restSchema.anyOf.length === 0) || (Array.isArray(restSchema.oneOf) &&
restSchema.oneOf.length === 0)); ensure the subsequent if (restSchema !==
undefined && !isEmptyRest) still includes boolean false cases so the original
JSON schema (e.g., { not: {} }) is preserved. Use the restSchema and isEmptyRest
symbols in openapi-utils.ts to locate and update this check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e9ff75d-c111-4ce7-8c55-504bb610554d

📥 Commits

Reviewing files that changed from the base of the PR and between 03c5dff and 7975181.

📒 Files selected for processing (2)
  • packages/openapi/src/openapi-utils.test.ts
  • packages/openapi/src/openapi-utils.ts

Comment thread packages/openapi/src/openapi-utils.ts Outdated
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Apr 21, 2026
@dinwwwh
Copy link
Copy Markdown
Member

dinwwwh commented Apr 21, 2026

Hey @ghdoergeloh, I’ve updated this PR and also fixed issue #1536. When you have a moment, could you please take another look?

dinwwwh and others added 2 commits April 21, 2026 10:22
- Extend isAnySchema to treat { not: {} } as "any" — this is the JSON
  Schema representation of z.instanceof(X), which has no meaningful
  constraint. Without this, toOpenAPIContent would emit a spurious
  application/json: { schema: not: {} } entry alongside binary content.
- In the generator, skip emitting requestBody when separateObjectSchema
  leaves an empty remainder (no properties, no required) after extracting
  path params — { type: 'object' } alone is not a meaningful body schema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ghdoergeloh
Copy link
Copy Markdown
Contributor Author

On top of @dinwwwh's fix, I've added two more things:

1. isAnySchema handles { not: {} }

z.instanceof(ReadableStream) has no JSON Schema equivalent and serializes to false, which toOpenAPISchema converts to { not: {} }. Dinwwwh's isAnySchema didn't cover this case, so toOpenAPIContent was still emitting a spurious application/json: { schema: { not: {} } } entry alongside the binary content type. Added a guard to treat { not: {} } ("not nothing" = everything allowed) as "any".

2. Generator skips requestBody when remainder has no properties (fixes #1536)

separateObjectSchema strips all path params from the input schema, leaving { type: 'object' } with no properties or required. isAnySchema({ type: 'object' }) returns false (because type is a logic keyword), so toOpenAPIContent still emits application/json: { schema: { type: 'object' } }, and the generator emits an empty requestBody. Added a guard in the generator to skip requestBody when the remainder schema has no properties — this is the right place since the generator decides what to emit, and separateObjectSchema's return type doesn't allow signalling "nothing left".

Comment thread packages/openapi/src/adapters/standard/openapi-codec.ts
Comment thread packages/openapi/src/schema-utils.ts Outdated
Comment thread packages/openapi/src/openapi-generator.ts Outdated
@dosubot dosubot Bot removed the lgtm This PR has been approved by a maintainer label Apr 21, 2026
Per review feedback: { not: {} } means "unsupported" (nothing matches),
not "any" — so it shouldn't be reinterpreted in isAnySchema. Instead,
suppress the application/json content entry in toOpenAPIContent when the
rest branch is false/{not:{}} AND a file branch was extracted, keeping
the original semantics for standalone unsupported schemas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Apr 21, 2026
@dinwwwh
Copy link
Copy Markdown
Member

dinwwwh commented Apr 22, 2026

Base on your update, I made a few tweaks. I think it looks really solid now - when you have a moment, please take a look @ghdoergeloh.

@ghdoergeloh
Copy link
Copy Markdown
Contributor Author

Much cleaner with isNeverSchema — that's the right name for the concept. And tracking omitResponseBody right where separateObjectSchema runs is a nicer boundary than my after-the-fact check on the combined schema. Thanks for the iteration! 🎉

@ghdoergeloh
Copy link
Copy Markdown
Contributor Author

Updated the PR description:

@dinwwwh dinwwwh merged commit b6b8746 into middleapi:main Apr 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request javascript Pull requests that update javascript code lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support return ReadableStream<binary> in .handler bug(openapi): empty requestBody emitted for POST routes with path-params-only input

2 participants