@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.
- package/dist/index.cjs +11 -1771
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -239
- package/dist/index.d.ts +7 -239
- package/dist/index.js +12 -1749
- package/dist/index.js.map +1 -1
- package/dist/templates/client.d.ts +47 -2
- package/docs/guides/authentication.md +16 -2
- package/docs/guides/base-url.md +46 -2
- package/docs/guides/calling-operations.md +10 -3
- package/docs/guides/error-handling.md +37 -11
- package/docs/guides/interceptors.md +36 -14
- package/docs/guides/migrate-to-v6.md +235 -0
- package/docs/guides/serialization.md +119 -57
- package/docs/guides/server-sent-events.md +134 -31
- package/docs/guides/transport.md +29 -19
- package/docs/options.md +22 -10
- package/docs/overview.md +5 -3
- package/kubb-extension.json +3 -7
- package/package.json +8 -10
- package/src/index.ts +0 -1
- package/src/plugin.ts +7 -74
- package/src/types.ts +5 -2
- package/templates/client.ts +65 -84
- package/dist/client-Dn80AjdR.d.ts +0 -39
- package/dist/core-D6FAGTsL.d.ts +0 -2
- package/dist/templates/core.d.ts +0 -2
- package/dist/templates/fetch.d.ts +0 -6
- package/dist/templates/serializers.d.ts +0 -1
- package/dist/templates/standardSchema.d.ts +0 -1
- package/src/generators/clientGenerator.tsx +0 -10
- package/src/templates.ts +0 -16
- package/templates/core.ts +0 -2
- package/templates/fetch.ts +0 -2
- package/templates/serializers.ts +0 -2
- package/templates/standardSchema.ts +0 -2
|
@@ -1,12 +1,57 @@
|
|
|
1
1
|
# Configure serialization
|
|
2
2
|
|
|
3
|
-
The generated client encodes parameters
|
|
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
|
-
##
|
|
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
|
-
|
|
39
|
+
### Header and cookie
|
|
8
40
|
|
|
9
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
`
|
|
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 {
|
|
112
|
+
import { XMLBuilder, XMLParser } from 'fast-xml-parser'
|
|
49
113
|
|
|
50
114
|
client.setConfig({
|
|
51
|
-
|
|
52
|
-
'application/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
'
|
|
130
|
+
mediaTypes: {
|
|
131
|
+
'multipart/mixed': { stream: (body, { contentType }) => readParts(body, boundaryOf(contentType)) },
|
|
80
132
|
},
|
|
81
133
|
})
|
|
82
134
|
```
|
|
83
135
|
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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`.
|
|
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
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
-
|
|
|
273
|
-
|
|
|
274
|
-
| The call returns
|
|
275
|
-
| Call it
|
|
276
|
-
| Loop over the events
|
|
277
|
-
| Other ways to loop
|
|
278
|
-
| `event.data`
|
|
279
|
-
| A failed call, by default
|
|
280
|
-
| With `throwOnError: false`
|
|
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
|
|
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'
|
package/docs/guides/transport.md
CHANGED
|
@@ -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
|
|
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?:
|
|
22
|
-
|
|
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
|
|
26
|
-
|
|
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
|
|
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 (
|
|
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
|