Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions reference/http/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,23 @@ contentTypes.set('text/xml', {

### Handler Interface

| Property | Type | Description |
| ----------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serialize(data)` | `(any) => Buffer \| Uint8Array \| string` | Serialize data for a response |
| `serializeStream(data)` | `(any) => Readable` | Serialize a streaming response body. Called once per response with the whole iterable (not once per chunk), and only when the response body is an iterable or async iterable. Returns a Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable). |
| `deserialize(buffer)` | `(Buffer \| string) => any` | Deserialize an incoming request body. String for `text/*` types, Buffer for binary types. |
| `q` | number (0–1) | Quality indicator for content negotiation. Defaults to `1`. |
| Property | Type | Description |
| ----------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serialize(data)` | `(any) => Buffer \| Uint8Array \| string \| Readable` | Serialize a complete response body. Used when the response body is not iterable, or when the handler defines no `serializeStream`. Most handlers return a string or a `Buffer`; the built-in `text/csv` handler returns a [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable), which Harper streams to the response. |
| `serializeStream(data)` | `(any) => Readable \| Buffer \| string` | Serialize a streaming response body. Called once per response with the whole iterable (not once per chunk), and only when the response body is an object that is iterable or async iterable. A Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable) is the usual return, but it is not the only one — see [Non-streaming returns from `serializeStream`](#non-streaming-returns-from-serializestream). |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The return type is incomplete relative to the subsection immediately below it: serializeStream is said to accept any iterable or async iterable and wrap it with Readable.from(), but neither is represented here. A typed custom handler returning a generator would therefore be rejected despite the documented runtime behavior. Add the iterable forms to the public type (and its core declaration), or narrow the prose to the supported contract.

| `deserialize(buffer)` | `(Buffer) => any` | Deserialize an incoming request body. Harper always passes a `Buffer`, including for `text/*` types — a text handler calls `buffer.toString()` itself. |
| `q` | number (0–1) | Quality indicator for content negotiation. Defaults to `1`. |

#### Non-streaming returns from `serializeStream`

`serializeStream` may return a `Readable`, a `Buffer`, a string, or any iterable or async iterable. Harper writes all of these to the response: a `Readable` is piped, an iterable is wrapped with `Readable.from()`, and a `Buffer` or string is sent as the whole body.

The union is not theoretical. Harper picks `serializeStream` over `serialize` whenever the response body is iterable, and a plain array is iterable — so a resource that returns an array reaches `serializeStream`. A handler that has nothing to stream in that case can serialize the value in one shot instead:

- The built-in `application/x-msgpack` handler streams only non-array iterables. For a plain array it returns a packed `Buffer`.
- The built-in NDJSON handler returns a string when it is handed a value that is not iterable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not a reachable serializeStream dispatch case: the preceding contract says Harper selects this method only when the response body is iterable or async iterable. A non-iterable response instead uses serialize, so this example does not demonstrate the new return union and can mislead readers about which method runs. Remove it, or explicitly frame it as a direct call outside Harper's normal response path.


One caveat if you return something other than a stream: when Brotli compression applies to the response (the client sent `Accept-Encoding: br` and `http.compressionThreshold` is non-zero), Harper pipes the value returned by `serializeStream` into the compressor. A `Buffer` or string has no `pipe`, so a handler that must work with compression enabled should return a stream — wrap a buffered result with `Readable.from([buffer])` if there is nothing to stream incrementally.

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.

medium

Since this Brotli compression caveat describes a scenario that leads to a runtime crash (TypeError: stream.pipe is not a function), it is highly recommended to format it using a :::caution block. This aligns with the established formatting patterns on this page (such as on lines 159 and 186) and ensures that developers scanning the documentation do not miss this critical warning.

Suggested change
One caveat if you return something other than a stream: when Brotli compression applies to the response (the client sent `Accept-Encoding: br` and `http.compressionThreshold` is non-zero), Harper pipes the value returned by `serializeStream` into the compressor. A `Buffer` or string has no `pipe`, so a handler that must work with compression enabled should return a stream — wrap a buffered result with `Readable.from([buffer])` if there is nothing to stream incrementally.
:::caution
One caveat if you return something other than a stream: when Brotli compression applies to the response (the client sent Accept-Encoding: br and http.compressionThreshold is non-zero), Harper pipes the value returned by serializeStream into the compressor. A Buffer or string has no pipe, so a handler that must work with compression enabled should return a stream — wrap a buffered result with Readable.from([buffer]) if there is nothing to stream incrementally.
:::
References
  1. When breaking down long, dense paragraphs or formatting warnings in Markdown documentation, maintain consistency with the existing formatting patterns of the page.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This documents a framework bug as a handler requirement. Buffer and string are now advertised as supported serializeStream results, but a routine request with Accept-Encoding: br can fail because the compressor assumes .pipe(). With the default compression threshold and browser clients, custom handlers cannot reliably use part of the documented API. Harper should normalize a non-stream result with Readable.from([value]) before the compression branch, so the return contract is independent of a response-header/configuration combination; then this caveat can be removed or reduced to normal streaming guidance.


---

Expand Down