@kubb/plugin-fetch 5.14.0-canary.20261109T134631 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,57 @@
1
1
  # Configure serialization
2
2
 
3
- The generated client encodes parameters using OpenAPI `style` and `explode`, serializes request bodies by content type, and decodes responses by media type. Each generated operation carries its parameter styles and content types, so most requests need no extra setup.
3
+ The generated client encodes parameters by their OpenAPI `style` and `explode`, and encodes and decodes bodies by their media type. Each generated operation carries its parameter styles and content types, so most requests need no extra setup.
4
4
 
5
- ## Override parameter serialization
5
+ ## Parameter styles
6
+
7
+ A parameter without `style` or `explode` in the spec uses the default for its location.
8
+
9
+ ### Query
10
+
11
+ Query parameters default to the `form` style. Arrays explode into repeated keys unless the spec
12
+ says otherwise, and `spaceDelimited`, `pipeDelimited`, and `deepObject` change how arrays and
13
+ objects collapse.
14
+
15
+ | Style | `explode` | Input | Result |
16
+ | ---------------- | --------- | ------------------- | ---------------- |
17
+ | `form` (default) | `true` | `{ id: [3, 4, 5] }` | `id=3&id=4&id=5` |
18
+ | `form` | `false` | `{ id: [3, 4, 5] }` | `id=3,4,5` |
19
+ | `spaceDelimited` | `false` | `{ id: [3, 4, 5] }` | `id=3%204%205` |
20
+ | `pipeDelimited` | `false` | `{ id: [3, 4, 5] }` | `id=3\|4\|5` |
21
+ | `deepObject` | `n/a` | `{ a: { b: 1 } }` | `a%5Bb%5D=1` |
22
+
23
+ With `explode: true`, `spaceDelimited` and `pipeDelimited` fall back to repeated keys like `form`,
24
+ so the delimiter only shows with `explode: false`.
25
+
26
+ ### Path
27
+
28
+ Path parameters default to the `simple` style, which emits the bare value. `label` prefixes a
29
+ `.` and `matrix` prefixes a `;name=` segment. The results below are the serialized segment for a
30
+ parameter named `id`.
31
+
32
+ | Style | `explode` | Input | Result |
33
+ | ------------------ | --------- | ---------------- | ----------------- |
34
+ | `simple` (default) | `false` | `[3, 4, 5]` | `3,4,5` |
35
+ | `label` | `true` | `[3, 4, 5]` | `.3.4.5` |
36
+ | `matrix` | `true` | `[3, 4, 5]` | `;id=3;id=4;id=5` |
37
+ | `simple` | `false` | `{ x: 1, y: 2 }` | `x,1,y,2` |
6
38
 
7
- Check the [default parameter styles and encoding](https://kubb.dev/docs/5.x/reference/serialization#parameter-styles) before replacing a serializer.
39
+ ### Header and cookie
8
40
 
9
- Override `serializer.query`, `.body`, or `.path` on the client. Omitted functions retain their defaults:
41
+ Header parameters use the `simple` style and cookie parameters use the `form` style. Both fix the
42
+ style and only let `explode` vary, so the metadata for these locations carries `explode` alone.
43
+ Header values are sent as-is, and cookie values are URL-encoded into a single `Cookie` header.
44
+
45
+ | Location | `explode` | Input | Result |
46
+ | -------- | --------- | --------------------------------- | ---------------------- |
47
+ | header | `false` | `[3, 4]` | `X-Ids: 3,4` |
48
+ | header | `true` | `{ role: 'admin' }` | `X-Filter: role=admin` |
49
+ | cookie | `false` | `{ session: 'abc', ids: [1, 2] }` | `session=abc; ids=1,2` |
50
+ | cookie | `true` | `{ ids: [1, 2] }` | `ids=1; ids=2` |
51
+
52
+ ## Override parameter serialization
53
+
54
+ To replace these defaults, override `serializer.query` or `serializer.path` on the client. Omitted functions keep their defaults:
10
55
 
11
56
  ```typescript
12
57
  import { client } from './gen/.kubb/client'
@@ -19,83 +64,104 @@ client.setConfig({
19
64
  })
20
65
  ```
21
66
 
22
- A serializer set this way runs for every call, but you can pass `serializer` on a single call to
23
- override only that request.
67
+ A serializer set this way runs for every call. Pass `serializer` on a single call to override only that request.
68
+
69
+ Kubb writes each serializer a spec needs into `.kubb/serializers/` and lists it in `.kubb/config.ts`, which `.kubb/client.ts` passes to `setConfig`. The core has no serializer of its own.
70
+
71
+ | Key in `serializer` | File | Written for |
72
+ | ------------------- | ------------------------------------------------------------------------------------ | ------------------------------ |
73
+ | `query` | `query.ts`, or `queryStyles.ts` for a spec with a styled query parameter | every spec |
74
+ | `path` | `path.ts`, plus `pathStyles.ts` for a spec with a `label` or `matrix` path parameter | every spec |
75
+ | `header` | `headers.ts` | a spec with a header parameter |
76
+ | `cookie` | `cookies.ts` | a spec with a cookie parameter |
77
+
78
+ A call that needs a serializer the spec did not register throws a `TypeError` that names the key, such as `serializer.cookie`. Set it on the client to send such a call. Without `serializer.header`, an array or object header value is sent as JSON.
79
+
80
+ ## Built-in media types
81
+
82
+ | Keys | Encodes | Decodes |
83
+ | ---------------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------ |
84
+ | `application/json`, `application/*+json`, `text/json` | JSON | JSON |
85
+ | `text/*` | a string | text |
86
+ | `multipart/form-data`, `application/x-www-form-urlencoded` | `FormData` or `URLSearchParams` from an object | `FormData` |
87
+ | `application/octet-stream`, `application/pdf`, `image/*`, `audio/*`, `video/*` | a `Blob` as is | `Blob` |
88
+ | `text/event-stream`, `application/jsonl`, `application/x-ndjson`, `application/json-seq` | | text, or item by item with `client.stream` |
89
+ | `*/*` | JSON for objects, strings and binary bodies as is | JSON, or text when the body is not JSON |
90
+
91
+ `.kubb/client.ts` registers a form media type only when the document has a body of that type, and the stream media types only when it streams. `multipartFormData` is in `.kubb/mediaTypes/formData.ts` and `urlencoded` in `.kubb/mediaTypes/urlencoded.ts`. The stream media types are `streamMediaTypes` in `.kubb/sse.ts`.
92
+
93
+ A key can be a pattern. The runtime lower-cases the type, drops its parameters, and tries the exact type, then a suffix such as `application/*+json`, then a wildcard such as `text/*`, then `*/*`. Write your own keys in lower case.
24
94
 
25
95
  ## Encode request bodies
26
96
 
27
- The request content type decides how the body is encoded. The default serializer handles the
28
- common types: a plain object becomes JSON, `multipart/form-data` becomes `FormData`, and
29
- `application/x-www-form-urlencoded` becomes `URLSearchParams`. Binary and already-encoded bodies
30
- (`FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer`, typed arrays, and strings) pass through
31
- untouched.
97
+ When an operation declares one request content type, Kubb sets it on the generated function, so you pass only the body. For an operation that accepts more than one, set `contentType` on the call. A body without a content type is sent as `application/json`, unless it is already encoded.
32
98
 
33
- When an operation declares a single request content type, Kubb sets it on the generated function,
34
- so you pass only the body. For an operation that accepts more than one, set `contentType` on the
35
- call.
99
+ Bodies that are already encoded (`FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer`, typed arrays, streams, and strings) pass through every built-in media type as they are. A string under a JSON media type is JSON-encoded.
36
100
 
37
101
  > [!NOTE]
38
- > When the body is `FormData`, the runtime removes any `Content-Type` header so the transport sets
39
- > it with the multipart boundary. You do not need to set `multipart/form-data` yourself, and a
40
- > value you set is dropped for that request.
102
+ > When the body is `FormData`, the runtime removes any `Content-Type` header so the transport sets it with the multipart boundary.
103
+
104
+ An XML, YAML, or CSV media type has no built-in handler, so an object sent under one throws a `TypeError`. Pass the encoded body as a string, or register a media type. The generated body type is the schema, such as `Pet`, so a string body needs a cast: `body: xml as unknown as AddPetBodyXml`.
41
105
 
42
- To encode a content type the default serializer does not handle, register a codec for that media
43
- type. `codecs` is keyed by content type. Each entry takes a `serialize` for the request body, a
44
- `deserialize` for the response, or both:
106
+ ## Add a media type
107
+
108
+ Register a handler under its media type with `mediaTypes`. `encode` gets the body and a context with the full `contentType`, and returns what `fetch` can send. `decode` gets the `Response`, so a binary format can read bytes:
45
109
 
46
110
  ```typescript
47
111
  import { client } from './gen/.kubb/client'
48
- import { stringify } from 'yaml'
112
+ import { XMLBuilder, XMLParser } from 'fast-xml-parser'
49
113
 
50
114
  client.setConfig({
51
- codecs: {
52
- 'application/yaml': { serialize: (body) => stringify(body) },
115
+ mediaTypes: {
116
+ 'application/xml': {
117
+ encode: (body) => new XMLBuilder().build(body),
118
+ decode: async (response) => new XMLParser().parse(await response.text()),
119
+ },
53
120
  },
54
121
  })
55
122
  ```
56
123
 
57
- ## Decode responses
124
+ Handlers merge per key over the built-ins, over earlier `setConfig` calls, and, when passed on a single call, over the client ones. An entry with only `decode` still encodes through the next match, such as `*/*`.
58
125
 
59
- The runtime reads the response `Content-Type` and decodes the body by it: JSON is parsed, text
60
- stays a string, and a binary type becomes a `Blob`. The negotiated media type is on the result as
61
- `contentType`, so a `switch (result.contentType)` narrows `data` for an operation that returns
62
- more than one.
63
-
64
- When that success response declares several content types that do not share one parser, the
65
- generated call sets `responseType` to a map from content type to parser, for example
66
- `{ 'audio/mpeg': 'blob', 'text/event-stream': 'stream' }`. The runtime matches the response
67
- `Content-Type` against the map, including a `type/*` wildcard, before the media-type heuristic.
68
- A schema with `format: binary` is a blob even when the media type is not a well-known binary type.
69
-
70
- To decode a media type the runtime does not handle, register a codec's `deserialize` for it.
71
- `deserialize` receives the raw body and the content type and returns the parsed value. It runs
72
- before validation, so the schema checks the parsed value:
126
+ A streaming format adds `stream`, which reads the 2xx body item by item. The runtime validates each item:
73
127
 
74
128
  ```typescript
75
- import { client } from './gen/.kubb/client'
76
-
77
129
  client.setConfig({
78
- codecs: {
79
- 'application/xml': { deserialize: (raw) => new DOMParser().parseFromString(raw as string, 'application/xml') },
130
+ mediaTypes: {
131
+ 'multipart/mixed': { stream: (body, { contentType }) => readParts(body, boundaryOf(contentType)) },
80
132
  },
81
133
  })
82
134
  ```
83
135
 
84
- Like the other config, `codecs` can also be passed on a single call, and a per-call entry merges
85
- over the client one for that content type.
136
+ ## Decode responses
137
+
138
+ The runtime decodes each body by the response `Content-Type`. The media type is on the result as `contentType`, so a `switch (result.contentType)` narrows `data` for an operation that returns more than one.
139
+
140
+ - A 2xx response without a `Content-Type` is decoded as the media type the call asked for in `contentType.response`, then as `*/*`.
141
+ - A 204, 205, or 304 response, a `HEAD` request, and an empty body give `data: undefined`.
142
+ - Invalid JSON under a JSON media type throws a `ParseError`. Catch it, or register a lenient `application/json` handler.
86
143
 
87
- When the runtime picks the wrong parse mode because a response omits its `Content-Type` or sets a
88
- misleading one, force the mode with `responseType` on the call. It accepts `'json'`, `'text'`,
89
- `'blob'`, `'arraybuffer'`, and `'document'`:
144
+ To skip the lookup for one call, set `parseAs` to `'json'`, `'text'`, `'blob'`, `'arrayBuffer'`, or `'stream'`. It applies to a 2xx body only. Kubb sets `parseAs: 'blob'` on an operation with only binary success content:
90
145
 
91
146
  ```typescript
92
- const { data } = await downloadInvoice({ path: { id: '123' }, responseType: 'blob' })
93
- // data is a Blob even when the server leaves Content-Type unset
147
+ const { data } = await getReport({ path: { id: '123' }, parseAs: 'text' })
94
148
  ```
95
149
 
96
- [Server-sent events](https://kubb.dev/extensions/kubb-labs/plugin-fetch/guides/server-sent-events) covers `responseType: 'stream'`.
150
+ The generated types follow what the built-in media types return. A response read as text, such as `application/xml`, `application/yaml`, or `text/csv`, is typed `string`, even when the document gives it an object schema. A vendor type such as `application/vnd.github.object` keeps its schema type, because the client parses its JSON body. A media type you register changes the value but not that type, so narrow or cast `data` to what it returns:
97
151
 
98
- For bidirectional formats, supply both `serialize` and `deserialize` in the media type codec, then set the operation's request and response `contentType`.
152
+ ```typescript
153
+ const result = await getPetById({ path: { petId: 1 }, contentType: { response: 'application/xml' } })
154
+ if (result.contentType === 'application/xml') {
155
+ const pet = result.data as unknown as Pet
156
+ }
157
+ ```
158
+
159
+ The examples set [`integerType: 'number'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#integertype) on the adapter, so an `int64` id such as `petId` is a `number`. With the default it is a `bigint` and the call takes `1n`.
160
+
161
+ [Server-sent events](https://kubb.dev/extensions/kubb-labs/plugin-fetch/guides/server-sent-events) covers stream media types and `client.stream`.
162
+
163
+ > [!NOTE]
164
+ > With the adapter's default [`integerType: 'bigint'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#integertype), a `format: int64` field is typed `bigint`, but `JSON.parse` returns a `number`. Set `integerType: 'number'` on `adapterOas`, or turn on [`validator: 'zod'`](#validate-responses), which converts the field to `bigint`. Either way, `JSON.parse` loses precision above `Number.MAX_SAFE_INTEGER`, so declare an exact 64-bit ID as a string in the document.
99
165
 
100
166
  ## Validate responses
101
167
 
@@ -107,14 +173,10 @@ import { pluginFetch } from '@kubb/plugin-fetch'
107
173
  pluginFetch({ validator: 'zod' })
108
174
  ```
109
175
 
110
- `'zod'` validates the success response body, and the error body when a non-2xx call does not
111
- throw. Use the object form to opt in per direction, where `request` validates the request
112
- body before the call goes out:
176
+ `'zod'` validates the success response body and the error body. Use the object form to opt in per direction, where `request` validates the request body before the call goes out:
113
177
 
114
178
  ```typescript
115
179
  pluginFetch({ validator: { request: 'zod', response: 'zod' } })
116
180
  ```
117
181
 
118
- With a validator set, Kubb passes the matching schema to each generated call, and the runtime
119
- parses the body through it. A body that does not match throws a `ParseError` with the schema's
120
- `issues`.
182
+ With a validator set, Kubb passes the matching schema to each generated call, and the runtime parses the decoded body through it. A body that does not match throws a `ParseError` with the schema's `issues`.
@@ -7,7 +7,7 @@ Each event on the wire has up to four fields, and a blank line ends it:
7
7
  | Field | Meaning |
8
8
  | ------- | -------------------------------------------------------------------------------- |
9
9
  | `event` | The event name. `message` when the server sends none. |
10
- | `data` | The payload. Parsed as JSON unless the document types it as a string. |
10
+ | `data` | The payload. Parsed as JSON when it is valid JSON, and kept as text otherwise. |
11
11
  | `id` | The last event ID. It carries over to later events and resumes a dropped stream. |
12
12
  | `retry` | The reconnect delay in milliseconds. |
13
13
 
@@ -33,7 +33,7 @@ For one payload on every event, such as a feed of pet changes. The item declares
33
33
 
34
34
  ### Describe it
35
35
 
36
- In OpenAPI 3.2, put one `oneOf` member per event name on `itemSchema`, with a `const` on `event`. Describe JSON inside `data` with `contentMediaType` and `contentSchema`:
36
+ In OpenAPI 3.2, put one `oneOf` member per event name on `itemSchema`, with a `const` on `event`. Point `data` at the payload schema with `$ref`. A `data` string described with `contentMediaType: application/json` and `contentSchema` is typed and parsed as that schema too:
37
37
 
38
38
  ```yaml
39
39
  /chat:
@@ -53,13 +53,16 @@ In OpenAPI 3.2, put one `oneOf` member per event name on `itemSchema`, with a `c
53
53
  id: { type: string }
54
54
  retry: { type: integer, minimum: 0 }
55
55
  oneOf:
56
- - properties:
57
- event: { const: message_delta }
58
- data:
59
- contentMediaType: application/json
60
- contentSchema: { $ref: '#/components/schemas/MessageDelta' }
61
- - properties:
62
- event: { const: ping }
56
+ - type: object
57
+ required: [event, data]
58
+ properties:
59
+ event: { type: string, const: message_delta }
60
+ data: { $ref: '#/components/schemas/MessageDelta' }
61
+ - type: object
62
+ required: [event]
63
+ properties:
64
+ event: { type: string, const: ping }
65
+ data: { type: string }
63
66
  ```
64
67
 
65
68
  In OpenAPI 3.0 and 3.1, put the same `oneOf` on `schema`, with a `discriminator` on `event`. Kubb reads `schema` as one event.
@@ -89,7 +92,7 @@ Kubb generates one union member per event name:
89
92
 
90
93
  ```typescript
91
94
  export type StreamChatStatus200 =
92
- | { data?: MessageDelta; event: 'message_delta'; id?: string; retry?: number }
95
+ | { data: MessageDelta; event: 'message_delta'; id?: string; retry?: number }
93
96
  | { data?: string; event: 'ping'; id?: string; retry?: number }
94
97
  ```
95
98
 
@@ -103,7 +106,7 @@ const { stream } = await streamChat({ body: { prompt: 'Hi' } })
103
106
  for await (const event of stream) {
104
107
  switch (event.event) {
105
108
  case 'message_delta':
106
- render(event.data?.text) // MessageDelta
109
+ render(event.data.text) // MessageDelta
107
110
  break
108
111
  case 'ping':
109
112
  break
@@ -173,16 +176,65 @@ for await (const { data } of stream) {
173
176
  }
174
177
  ```
175
178
 
179
+ The examples set [`integerType: 'number'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#integertype) on the adapter, so an `int64` id such as `petId` is a `number`. With the default it is a `bigint` and the call takes `1n`.
180
+
181
+ ## JSON Lines
182
+
183
+ An operation whose only success content type is a JSON sequence, such as `application/jsonl`, `application/x-ndjson`, or `application/json-seq`, streams one item per line. Each line is parsed as JSON, so the loop gets each item as soon as its line arrives instead of waiting for the whole body. An `application/json-seq` record must fit on one line. A record that spans several lines fails to parse.
184
+
185
+ ### Describe it
186
+
187
+ Point `itemSchema` at the schema of one line:
188
+
189
+ ```yaml
190
+ /logs:
191
+ get:
192
+ operationId: streamLogs
193
+ responses:
194
+ '200':
195
+ description: One LogEntry per line
196
+ content:
197
+ application/jsonl:
198
+ itemSchema:
199
+ $ref: '#/components/schemas/LogEntry'
200
+ ```
201
+
202
+ In OpenAPI 3.0 and 3.1, `schema` on one of these media types describes one line.
203
+
204
+ ### The stream
205
+
206
+ ```text
207
+ {"level": "info", "message": "started"}
208
+ {"level": "warn", "message": "slow"}
209
+ ```
210
+
211
+ The call asks for the declared media type in `Accept`. A line may end with CRLF, CR, or LF, blank lines are skipped, and a last line without a line break still counts. A line that is not valid JSON throws a `SyntaxError` from the loop.
212
+
213
+ ### Use it
214
+
215
+ Each item is a `LogEntry`, with no event wrapper:
216
+
217
+ ```typescript
218
+ import { streamLogs } from './gen/clients/streamLogs'
219
+
220
+ for await (const entry of await streamLogs()) {
221
+ console.info(entry.level, entry.message) // LogEntry
222
+ }
223
+ ```
224
+
225
+ `terminator`, `maxEventSize`, and `signal` work per line. `transform` only applies to Server-Sent Events. With `validator: 'zod'`, the validator runs on each line. `lastEventId` stays empty, since JSON Lines has no event IDs.
226
+
176
227
  ## Stream options
177
228
 
178
229
  Pass these on the call, next to its request params:
179
230
 
180
- | Option | What it does |
181
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- |
182
- | `terminator` | Ends the stream when `data` equals this value, such as `[DONE]`. Without it, the sentinel reaches your loop. |
183
- | `transform` | Maps each event before validation, for example to camelCase its keys. |
184
- | `maxEventSize` | Largest event in characters. A larger one throws a `RangeError`. Defaults to `16 * 1024 * 1024`. Set `Infinity` to turn it off. |
185
- | `signal` | An `AbortSignal` that ends the request and the loop. |
231
+ | Option | What it does |
232
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
233
+ | `terminator` | Ends the stream when `data` equals this value, such as `[DONE]`. Without it, the sentinel reaches your loop, or fails the `validator` when it does not match the event schema. |
234
+ | `transform` | Maps each event before validation, for example to camelCase its keys. |
235
+ | `maxEventSize` | Largest event in characters. A larger one throws a `RangeError`. Defaults to `16 * 1024 * 1024`. Set `Infinity` to turn it off. |
236
+ | `signal` | An `AbortSignal` that ends the request and the loop. |
237
+ | `eventShape` | What the validator checks: `data` for the payload, `event` for the whole event. Generated calls set it. |
186
238
 
187
239
  To stop reading, `break` out of the loop. The runtime then cancels the reader and releases the connection.
188
240
 
@@ -202,9 +254,19 @@ const { stream } = await streamChat({
202
254
 
203
255
  Set `validate: false` on the call or the client to skip it. See [Handle errors](https://kubb.dev/extensions/kubb-labs/plugin-fetch/guides/error-handling) for the validation options.
204
256
 
257
+ ## Read the response headers
258
+
259
+ A stream result carries the response `headers`, as a regular result does. When the response declares headers, such as an `X-Request-ID`, the call returns `StreamResultWithHeaders`, so `headers.get` suggests their names for the narrowed `status`:
260
+
261
+ ```typescript
262
+ const result = await streamChat({ body: { prompt: 'Hi' } })
263
+
264
+ console.info(result.headers.get('X-Request-ID'))
265
+ ```
266
+
205
267
  ## Resume after a drop
206
268
 
207
- A dropped connection ends the loop with the transport's error. The result's `lastEventId` is the `id` of the last event the server completed, so a new call with a `Last-Event-ID` header continues where the stream stopped. The runtime does not reconnect by itself, because many streams, such as a chat completion, end on purpose, and the retry policy belongs to your app:
269
+ A dropped connection ends the loop with the transport's error. The result's `lastEventId` is the `id` of the last event the server completed, so a new call with a `Last-Event-ID` header continues where the stream stopped. Declare `Last-Event-ID` as a header parameter of the operation, so the call accepts it in `headers`. The runtime does not reconnect by itself, because many streams, such as a chat completion, end on purpose, and the retry policy belongs to your app:
208
270
 
209
271
  ```typescript
210
272
  let lastEventId = ''
@@ -239,7 +301,7 @@ try {
239
301
  }
240
302
  ```
241
303
 
242
- With `throwOnError: false`, the call resolves instead of throwing, as a regular call does. The result then carries the `status` and the `error` body, its `data` is `undefined`, and its stream yields nothing. Check `data` rather than `error`, because an empty error body leaves `error` undefined. Checking `status` narrows `error` to the body the document declares for that status:
304
+ With `throwOnError: false`, the call resolves instead of throwing, as a regular call does. The result then carries the `status` and the `error` body, its `data` is `undefined`, and its stream yields nothing. Check `data` to tell the two apart. An empty error body sets `error` to `{ status, statusText }`. Checking `status` narrows `error` to the body the document declares for that status:
243
305
 
244
306
  ```typescript
245
307
  const result = await streamChat({ body, throwOnError: false })
@@ -253,10 +315,51 @@ As on a regular call, `.unwrap()` resolves to the events. A failed call rejects
253
315
  for await (const event of await streamChat({ body }).unwrap()) render(event)
254
316
  ```
255
317
 
256
- A stream call that gets a JSON response, such as an error body, parses it like any other call, so a 2xx JSON body yields an empty stream. An operation that declares another content type next to `text/event-stream` is generated as a normal call.
318
+ A stream call that gets a JSON error body parses it like any other call. A 2xx answer that is not an event stream, such as a JSON body, throws a `TypeError` with the body as its `cause`, and an empty 2xx answer ends as an empty stream. An operation that declares another content type next to `text/event-stream` or a JSON Lines type is a normal call. See [JSON or an event stream](#json-or-an-event-stream).
257
319
 
258
320
  Declare heartbeat and `error` events in the document, so each has a `case` in your `switch`.
259
321
 
322
+ ## JSON or an event stream
323
+
324
+ Some APIs, such as Wikimedia EventStreams, declare the same response as `application/json` and as `text/event-stream`. Kubb generates a normal call for it, which returns a `RequestResult`. The call sends `Accept: text/event-stream`, so the server streams events. To ask for JSON, pass `contentType: { response: 'application/json' }`. Wikimedia answers JSON with an endless JSON Lines body, so also pass `parseAs: 'stream'` and read `result.data` with `parseJsonLines({ stream: result.data })` from `.kubb/client`. Without it, the call waits for a body that never ends.
325
+
326
+ Check `contentType` on the result. For `text/event-stream`, `data` is the raw `ReadableStream<Uint8Array>`. Pass it to `parseEventStream` from `.kubb/client` to read the events:
327
+
328
+ ```typescript
329
+ import { parseEventStream } from './gen/.kubb/client'
330
+ import { getV2StreamRecentchange } from './gen/clients/getV2StreamRecentchange'
331
+
332
+ const result = await getV2StreamRecentchange({ signal })
333
+
334
+ if (result.contentType === 'text/event-stream') {
335
+ for await (const { data } of parseEventStream(result.data)) render(data)
336
+ } else {
337
+ render(result.data) // the JSON body
338
+ }
339
+ ```
340
+
341
+ To get a stream result with `lastEventId`, send the same request with `client.stream`. It throws a `TypeError` when the server answers with JSON:
342
+
343
+ ```typescript
344
+ import { client } from './gen/.kubb/client'
345
+
346
+ const result = await client.stream({ method: 'GET', url: '/v2/stream/recentchange', eventShape: 'data', signal })
347
+ for await (const event of result) render(event.data)
348
+ ```
349
+
350
+ ## Stream with the client
351
+
352
+ `client.stream(config)` runs the same interceptors and transport as `client(config)`. On a 2xx answer, it reads the body item by item and validates each item with `validator.response`. A non-2xx body is decoded whole, so `error` stays a value. Type it with the responses record, `ThrowOnError`, and the item type, which defaults to `ServerSentEvent` around the success body:
353
+
354
+ ```typescript
355
+ import { client, type SuccessOf } from './gen/.kubb/client'
356
+ import type { StreamLogsResponses } from './gen/types/StreamLogs'
357
+
358
+ const result = await client.stream<StreamLogsResponses, true, SuccessOf<StreamLogsResponses>>({ method: 'GET', url: '/logs' })
359
+ ```
360
+
361
+ To stream another format, register a `stream` handler, as [Configure serialization](https://kubb.dev/extensions/kubb-labs/plugin-fetch/guides/serialization#add-a-media-type) shows.
362
+
260
363
  ## Return only the events
261
364
 
262
365
  With `returnType: 'data'` in the plugin options, a stream call returns its events, as a regular call returns only its body:
@@ -269,16 +372,16 @@ pluginFetch({ returnType: 'data' })
269
372
 
270
373
  Both modes read the same events. The table compares them for one call:
271
374
 
272
- | | `returnType: 'full'` (default) | `returnType: 'data'` |
273
- | ----------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
274
- | The call returns | `Unwrappable<StreamResult<…>>` | `Promise<UnwrappedStreamResult<…>>` |
275
- | Call it | `const result = await streamPetEvents({ path: { petId: 1 } })` | `const events = await streamPetEvents({ path: { petId: 1 } })` |
276
- | Loop over the events | `for await (const event of result.stream)` | `for await (const event of events)` |
277
- | Other ways to loop | `result`, `result.data`, or `.unwrap()` | none |
278
- | `event.data` | `PetEvent` | `PetEvent` |
279
- | A failed call, by default | throws a `ResponseError` | throws a `ResponseError` |
280
- | With `throwOnError: false` | the full result | the full result, as in `'full'` |
281
- | `status`, `response`, `lastEventId` | available | only with `throwOnError: false` |
375
+ | | `returnType: 'full'` (default) | `returnType: 'data'` |
376
+ | ---------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
377
+ | The call returns | `Unwrappable<StreamResult<…>>` | `Promise<UnwrappedStreamResult<…>>` |
378
+ | Call it | `const result = await streamPetEvents({ path: { petId: 1 } })` | `const events = await streamPetEvents({ path: { petId: 1 } })` |
379
+ | Loop over the events | `for await (const event of result.stream)` | `for await (const event of events)` |
380
+ | Other ways to loop | `result`, `result.data`, or `.unwrap()` | none |
381
+ | `event.data` | `PetEvent` | `PetEvent` |
382
+ | A failed call, by default | throws a `ResponseError` | throws a `ResponseError` |
383
+ | With `throwOnError: false` | the full result | the full result, as in `'full'` |
384
+ | `status`, `headers`, `response`, `lastEventId` | available | only with `throwOnError: false` |
282
385
 
283
386
  Loop over the events the call resolves to:
284
387
 
@@ -323,7 +426,7 @@ The events alone carry no `response` or `lastEventId`. To resume a dropped strea
323
426
 
324
427
  ## The runtime files
325
428
 
326
- `.kubb/sse.ts` holds the parser, `toEventStream`, and the `ServerSentEvent`, `TypedEvent`, `EventStreamOptions`, and `EventStreamResult` types. A stream operation takes `StreamOptions` and returns `StreamResult`, the stream versions of `Options` and `RequestResult`. Kubb writes it only when the document has a `text/event-stream` operation, and `.kubb/client.ts` then re-exports it, so the types import from either file. Kubb regenerates both on each run, so do not edit them:
429
+ `.kubb/sse.ts` holds `parseEventStream`, `parseJsonLines`, and the `eventStream`, `jsonLines`, and `streamMediaTypes` media types. The `ServerSentEvent`, `TypedEvent`, `EventStreamOptions`, `StreamOptions`, `StreamResult`, and `StreamResultWithHeaders` types live in `.kubb/core.ts`. A stream operation takes `StreamOptions` and returns `StreamResult`, or `StreamResultWithHeaders` when the response declares headers. These types are the stream versions of `Options`, `RequestResult`, and `RequestResultWithHeaders`. `.kubb/client.ts` re-exports `sse.ts`, so everything imports from the client. Kubb regenerates these files on each run, so do not edit them:
327
430
 
328
431
  ```typescript
329
432
  import type { ServerSentEvent } from './gen/.kubb/client'
@@ -6,10 +6,10 @@ The transport is a function. Use interceptors for request headers or response lo
6
6
 
7
7
  ## Write a transport function
8
8
 
9
- `@kubb/plugin-fetch` types the transport as a function that receives a fully resolved request and returns a result:
9
+ `@kubb/plugin-fetch` types the transport as a function that receives a fully resolved request and returns a web `Response`:
10
10
 
11
11
  ```typescript
12
- type Transport = (request: ResolvedRequest) => Promise<TransportResult>
12
+ type Transport = (request: ResolvedRequest) => Promise<Response | TransportResult>
13
13
 
14
14
  type ResolvedRequest = {
15
15
  url: string
@@ -18,22 +18,37 @@ type ResolvedRequest = {
18
18
  body?: RequestBody
19
19
  signal?: AbortSignal
20
20
  credentials?: RequestCredentials
21
- options?: FetchOptions
22
- responseType?: ResponseType
21
+ options?: NativeOptions
22
+ // Set when the call streams the response. Do not buffer the body.
23
+ stream?: boolean
23
24
  }
24
25
 
25
- type TransportResult<TData = unknown> = {
26
- data: TData
27
- status: number
28
- statusText: string
29
- headers: Headers
30
- contentType?: string
26
+ type TransportResult = {
27
+ webResponse: Response
31
28
  request: Request
32
29
  response: Response
33
30
  }
34
31
  ```
35
32
 
36
- The core hands you a `ResolvedRequest` with the URL built, the query and body serialized, and auth headers set. Return the parsed `data` along with the native `request` and `response`, so status, headers, and the raw body stay reachable on the result.
33
+ The `ResolvedRequest` has the URL built, the query and body encoded, and auth headers set. Return the `Response` with its body unread. Return a `TransportResult` to set the native `request` and `response` on results and errors.
34
+
35
+ ## Send with another library
36
+
37
+ A library that returns a `Response`, such as [ky](https://github.com/sindresorhus/ky), needs only the send. `toRequestInit(request)` gives the `RequestInit` that `fetch` and ky take:
38
+
39
+ ```typescript
40
+ import ky from 'ky'
41
+ import { client, toRequestInit } from './gen/.kubb/client'
42
+
43
+ client.setConfig({
44
+ // The client splits success from error itself, so ky must not throw or retry on an error status. ky's `timeout`
45
+ // (10 seconds by default) only waits for the response headers and never ends a started body or stream. Turn it
46
+ // off and bound the whole call with `signal: AbortSignal.timeout(ms)`.
47
+ transport: (request) => ky(request.url, { ...toRequestInit(request), throwHttpErrors: false, retry: 0, timeout: false }),
48
+ })
49
+ ```
50
+
51
+ The library must hand back the body unread. `result.request` is then a `Request` built from the resolved request, without its body, since the library keeps its own. With a relative URL, it is the resolved request itself.
37
52
 
38
53
  ## Mock the network in tests
39
54
 
@@ -44,20 +59,15 @@ import { createClient } from './gen/.kubb/client'
44
59
  import { getPetById } from './gen/clients/getPetById'
45
60
 
46
61
  const testClient = createClient({
47
- transport: async (request) => ({
48
- data: { id: 1, name: 'Fluffy' },
49
- status: 200,
50
- statusText: 'OK',
51
- headers: new Headers(),
52
- request: new Request(request.url),
53
- response: new Response(),
54
- }),
62
+ transport: async () => Response.json({ id: 1, name: 'Fluffy' }),
55
63
  })
56
64
 
57
65
  const { data } = await getPetById({ path: { petId: 1 }, client: testClient })
58
66
  // ^ { id: 1, name: 'Fluffy' }
59
67
  ```
60
68
 
69
+ The examples set [`integerType: 'number'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#integertype) on the adapter, so an `int64` id such as `petId` is a `number`. With the default it is a `bigint` and the call takes `1n`.
70
+
61
71
  Pass the isolated client per operation or to a query plugin.
62
72
 
63
73
  ## Where to set it