@kubb/plugin-fetch 5.13.0-canary.20261105T105941 → 5.13.0

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,94 +1,330 @@
1
1
  # Server-sent events
2
2
 
3
- Operations returning `text/event-stream` produce a typed stream rather than a `RequestResult`.
3
+ An operation that returns `text/event-stream` gives you a typed stream of events instead of a `RequestResult`. Kubb takes the event types from the OpenAPI document. The runtime in `.kubb/sse.ts` parses the stream the way the WHATWG event stream specification does.
4
4
 
5
- ## Consume a stream
5
+ Each event on the wire has up to four fields, and a blank line ends it:
6
6
 
7
- Read the `EventStreamResult.stream` with `for await`:
7
+ | Field | Meaning |
8
+ | ------- | -------------------------------------------------------------------------------- |
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. |
11
+ | `id` | The last event ID. It carries over to later events and resumes a dropped stream. |
12
+ | `retry` | The reconnect delay in milliseconds. |
13
+
14
+ ## Pick a shape
15
+
16
+ Kubb reads the stream's item schema in one of two shapes, and the item's properties decide which:
17
+
18
+ ::field-group
19
+
20
+ :::field{name="event"}
21
+ For several named events, such as `message_delta` and `ping`. The item declares `event` or `data`, and the stream yields `TypedEvent<Item>`. See [Typed events](#typed-events).
22
+ :::
23
+
24
+ :::field{name="data"}
25
+ For one payload on every event, such as a feed of pet changes. The item declares neither, and the stream yields `ServerSentEvent<Item>`. See [Payload only](#payload-only).
26
+ :::
27
+
28
+ ::
29
+
30
+ `examples/advanced` shows both shapes: `streamOrderEvents` is typed by event, and `streamPetEvents` is typed by payload.
31
+
32
+ ## Typed events
33
+
34
+ ### Describe it
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`:
37
+
38
+ ```yaml
39
+ /chat:
40
+ post:
41
+ operationId: streamChat
42
+ responses:
43
+ '200':
44
+ description: One event per oneOf member
45
+ content:
46
+ text/event-stream:
47
+ itemSchema:
48
+ type: object
49
+ required: [event]
50
+ properties:
51
+ data: { type: string }
52
+ event: { type: string }
53
+ id: { type: string }
54
+ retry: { type: integer, minimum: 0 }
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 }
63
+ ```
64
+
65
+ In OpenAPI 3.0 and 3.1, put the same `oneOf` on `schema`, with a `discriminator` on `event`. Kubb reads `schema` as one event.
66
+
67
+ ### The stream
68
+
69
+ ```text
70
+ event: message_delta
71
+ id: 1
72
+ data: {"text": "Hel"}
73
+
74
+ event: ping
75
+ data: keep-alive
76
+
77
+ ```
78
+
79
+ This stream yields two events. The `id` carries over to the second one:
80
+
81
+ ```typescript
82
+ { event: 'message_delta', id: '1', data: { text: 'Hel' } }
83
+ { event: 'ping', id: '1', data: 'keep-alive' }
84
+ ```
85
+
86
+ ### Use it
87
+
88
+ Kubb generates one union member per event name:
89
+
90
+ ```typescript
91
+ export type StreamChatStatus200 =
92
+ | { data?: MessageDelta; event: 'message_delta'; id?: string; retry?: number }
93
+ | { data?: string; event: 'ping'; id?: string; retry?: number }
94
+ ```
95
+
96
+ A `switch` on `event` narrows `data` in each `case`:
8
97
 
9
98
  ```typescript
10
- import { streamEvents } from './gen/clients/streamEvents'
99
+ import { streamChat } from './gen/clients/streamChat'
11
100
 
12
- const { stream } = await streamEvents({})
101
+ const { stream } = await streamChat({ body: { prompt: 'Hi' } })
13
102
 
14
103
  for await (const event of stream) {
15
- console.info(event.data)
104
+ switch (event.event) {
105
+ case 'message_delta':
106
+ render(event.data?.text) // MessageDelta
107
+ break
108
+ case 'ping':
109
+ break
110
+ }
16
111
  }
17
112
  ```
18
113
 
19
- `data` is typed from the operation's response schema. Each event also carries the optional SSE
20
- fields, so you can branch on the event name or read the id:
114
+ The stream yields every event the server sends. An event name the document does not declare reaches the `default` of your `switch`, and with `validator: 'zod'` the event schema rejects it, as it would any other response that does not match the document.
115
+
116
+ ## Payload only
117
+
118
+ ### Describe it
119
+
120
+ Point `itemSchema` at the payload. Keep `schema: { type: string }` next to it, so tools that read OpenAPI 3.0 or 3.1 still accept the document:
121
+
122
+ ```yaml
123
+ /pet/{petId}/events:
124
+ get:
125
+ operationId: streamPetEvents
126
+ responses:
127
+ '200':
128
+ description: Server-Sent Events stream of pet changes
129
+ content:
130
+ text/event-stream:
131
+ schema:
132
+ type: string
133
+ itemSchema:
134
+ $ref: '#/components/schemas/PetEvent'
135
+ ```
136
+
137
+ In OpenAPI 3.0 and 3.1, a `schema` without `event` or `data` properties has the same shape.
138
+
139
+ ### The stream
140
+
141
+ ```text
142
+ data: {"type": "updated", "pet": {"id": 1, "name": "Rex"}}
143
+
144
+ data: {"type": "deleted", "pet": {"id": 1, "name": "Rex"}}
145
+
146
+ ```
147
+
148
+ This stream yields two events. The server sends no `event`, so each one is named `message`:
21
149
 
22
150
  ```typescript
23
- type ServerSentEvent<TData> = {
24
- data: TData
25
- event?: string
26
- id?: string
27
- retry?: number
151
+ { event: 'message', data: { type: 'updated', pet: { id: 1, name: 'Rex' } } }
152
+ { event: 'message', data: { type: 'deleted', pet: { id: 1, name: 'Rex' } } }
153
+ ```
154
+
155
+ ### Use it
156
+
157
+ Each event is a `ServerSentEvent<PetEvent>`:
158
+
159
+ ```typescript
160
+ type ServerSentEvent<TData> = { data: TData; event: string; id?: string; retry?: number }
161
+ ```
162
+
163
+ Read `data` straight from each event. The result also holds the native `response` of the first connection:
164
+
165
+ ```typescript
166
+ import { streamPetEvents } from './gen/clients/streamPetEvents'
167
+
168
+ const { stream, response } = await streamPetEvents({ path: { petId: 1 } })
169
+ console.info(response.status) // 200
170
+
171
+ for await (const { data } of stream) {
172
+ console.info(data.type, data.pet.name) // PetEvent
28
173
  }
29
174
  ```
30
175
 
31
- The runtime parses each event's `data` as JSON when it is valid and keeps it as the raw string
32
- otherwise.
176
+ ## Stream options
177
+
178
+ Pass these on the call, next to its request params:
33
179
 
34
- ## Stop early
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. |
35
186
 
36
- Break out of the loop to stop reading. The underlying reader is canceled when the iterator
37
- finishes, so an early `break` releases the stream:
187
+ To stop reading, `break` out of the loop. The runtime then cancels the reader and releases the connection.
188
+
189
+ ## Validate events
190
+
191
+ With `validator: 'zod'` in the plugin options, the validator runs on each event: the whole event for typed events, `data` for a payload. A failing event calls `onValidationError`. Return `{ value }` to keep going, or the loop throws a `ParseError`:
38
192
 
39
193
  ```typescript
40
- const { stream } = await streamEvents({})
194
+ const { stream } = await streamChat({
195
+ body,
196
+ onValidationError: (error, { value }) => {
197
+ console.warn(error.issues)
198
+ return { value }
199
+ },
200
+ })
201
+ ```
41
202
 
42
- for await (const event of stream) {
43
- if (event.event === 'done') break
44
- render(event.data)
203
+ 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
+
205
+ ## Resume after a drop
206
+
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:
208
+
209
+ ```typescript
210
+ let lastEventId = ''
211
+
212
+ for (let attempt = 0; attempt < 3; attempt++) {
213
+ const result = await streamChat({ body, headers: lastEventId ? { 'Last-Event-ID': lastEventId } : {} })
214
+ try {
215
+ for await (const event of result.stream) render(event)
216
+ break
217
+ } catch (error) {
218
+ if (ResponseError.is(error)) throw error
219
+ lastEventId = result.lastEventId || lastEventId
220
+ }
45
221
  }
46
222
  ```
47
223
 
48
- To stop a stream from outside the loop, pass an `AbortSignal` and abort it. Aborting ends the
49
- request and the iteration:
224
+ `lastEventId` is empty until the server sends an `id`, and an empty `id:` field clears it. An `id` that contains a NULL character is ignored. Each event carries the server's `retry` hint as `retry`.
225
+
226
+ ## Handle errors
227
+
228
+ A non-2xx response throws a `ResponseError`, from the call or from the loop:
50
229
 
51
230
  ```typescript
52
- const controller = new AbortController()
53
- const { stream } = await streamEvents({ signal: controller.signal })
231
+ import { ResponseError } from './gen/.kubb/client'
54
232
 
55
- // elsewhere, for example on component unmount
56
- controller.abort()
233
+ try {
234
+ const { stream } = await streamChat({ body })
235
+ for await (const event of stream) render(event)
236
+ } catch (error) {
237
+ if (!ResponseError.is(error)) throw error
238
+ console.error('stream rejected', error.status)
239
+ }
57
240
  ```
58
241
 
59
- ## Read the response
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:
60
243
 
61
- Read status and headers from the native `response` before iterating:
244
+ ```typescript
245
+ const result = await streamChat({ body, throwOnError: false })
246
+ if (!result.data) console.error('stream rejected', result.status, result.error)
247
+ else for await (const event of result.data) render(event)
248
+ ```
249
+
250
+ As on a regular call, `.unwrap()` resolves to the events. A failed call rejects with a `ResponseError` by default, and with the `error` body when the call sets `throwOnError: false`:
62
251
 
63
252
  ```typescript
64
- const { stream, response } = await streamEvents({})
253
+ for await (const event of await streamChat({ body }).unwrap()) render(event)
254
+ ```
65
255
 
66
- console.info(response.status) // 200
67
- console.info(response.headers.get('x-stream-id'))
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.
68
257
 
69
- for await (const event of stream) {
70
- handle(event.data)
71
- }
258
+ Declare heartbeat and `error` events in the document, so each has a `case` in your `switch`.
259
+
260
+ ## Return only the events
261
+
262
+ With `returnType: 'data'` in the plugin options, a stream call returns its events, as a regular call returns only its body:
263
+
264
+ ```typescript
265
+ import { pluginFetch } from '@kubb/plugin-fetch'
266
+
267
+ pluginFetch({ returnType: 'data' })
72
268
  ```
73
269
 
74
- ## Handle errors
270
+ Both modes read the same events. The table compares them for one call:
271
+
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` |
282
+
283
+ Loop over the events the call resolves to:
284
+
285
+ ```typescript
286
+ import { streamOrderEvents } from './gen/clients/streamOrderEvents'
287
+ import { streamPetEvents } from './gen/clients/streamPetEvents'
288
+
289
+ const events = await streamOrderEvents({ path: { orderId: 7 } })
290
+
291
+ for await (const event of events) {
292
+ if (event.event === 'order_placed') render(event.data) // Order
293
+ }
294
+
295
+ for await (const { data } of await streamPetEvents({ path: { petId: 1 } })) {
296
+ console.info(data.type) // PetEvent
297
+ }
298
+ ```
75
299
 
76
- Catch non-2xx responses around the call and iteration, and rethrow other errors such as connection failures:
300
+ A failed call throws a `ResponseError` before the first event, and a dropped connection throws from the loop:
77
301
 
78
302
  ```typescript
79
303
  import { ResponseError } from './gen/.kubb/client'
80
304
 
81
305
  try {
82
- const { stream } = await streamEvents({})
83
- for await (const event of stream) {
84
- handle(event.data)
85
- }
306
+ for await (const event of await streamPetEvents({ path: { petId: 1 } })) render(event)
86
307
  } catch (error) {
87
308
  if (!ResponseError.is(error)) throw error
88
309
  console.error('stream rejected', error.status)
89
310
  }
90
311
  ```
91
312
 
92
- > [!NOTE]
93
- > A drop mid-stream ends the `for await` loop. Track the last event's `id` if you need to reconnect
94
- > from where the stream stopped.
313
+ With `throwOnError: false`, the call returns the full result instead, as a regular call does in this mode. The events are its `data` on a 2xx response, and checking `status` narrows `error`:
314
+
315
+ ```typescript
316
+ const result = await streamPetEvents({ path: { petId: 1 }, throwOnError: false })
317
+
318
+ if (!result.data) console.error('stream rejected', result.status, result.error)
319
+ else for await (const event of result.data) render(event)
320
+ ```
321
+
322
+ The events alone carry no `response` or `lastEventId`. To resume a dropped stream, use the default `returnType: 'full'`, or pass `throwOnError: false` to get the full result.
323
+
324
+ ## The runtime files
325
+
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:
327
+
328
+ ```typescript
329
+ import type { ServerSentEvent } from './gen/.kubb/client'
330
+ ```
@@ -20,7 +20,7 @@
20
20
  "dependencies": ["plugin-ts"],
21
21
  "optionalDependencies": ["plugin-zod"],
22
22
  "compatibility": {
23
- "kubb": ">=5.14.6",
23
+ "kubb": ">=5.16.0",
24
24
  "node": ">=22.22"
25
25
  },
26
26
  "role": "client",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-fetch",
3
- "version": "5.13.0-canary.20261105T105941",
3
+ "version": "5.13.0",
4
4
  "description": "Generate a slim, type-safe HTTP client with Kubb based on the native Fetch api.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -80,14 +80,15 @@
80
80
  "registry": "https://registry.npmjs.org/"
81
81
  },
82
82
  "dependencies": {
83
- "@kubb/kit": "^5.15.4",
84
- "@kubb/plugin-ts": "^5.5.0-canary.20261105T105941",
85
- "@kubb/plugin-zod": "^5.9.0-canary.20261105T105941",
86
- "@kubb/renderer-jsx": "^5.15.4"
83
+ "@kubb/kit": "^5.16.0",
84
+ "@kubb/plugin-client": "^5.10.0",
85
+ "@kubb/plugin-ts": "^5.4.1",
86
+ "@kubb/plugin-zod": "^5.8.2",
87
+ "@kubb/renderer-jsx": "^5.16.0"
87
88
  },
88
89
  "devDependencies": {
89
- "@internals/client": "0.1.0-canary.20261105T105941",
90
- "@internals/shared": "0.1.0-canary.20261105T105941",
90
+ "@internals/client": "0.0.0",
91
+ "@internals/shared": "0.0.0",
91
92
  "typescript": "^7.0.2"
92
93
  },
93
94
  "peerDependencies": {},
@@ -1,4 +1,5 @@
1
1
  import { createClientGenerator } from '@internals/client'
2
+ import { sseTemplatePath } from '../templates.ts'
2
3
  import type { PluginFetch } from '../types.ts'
3
4
 
4
5
  /**
@@ -6,4 +7,4 @@ import type { PluginFetch } from '../types.ts'
6
7
  * operation using the shared `Operation` component: a grouped `<Name>Request` type and a function
7
8
  * that forwards a single `options` object to the bundled `client` and returns the `RequestResult`.
8
9
  */
9
- export const clientGenerator = createClientGenerator<PluginFetch>('fetch')
10
+ export const clientGenerator = createClientGenerator<PluginFetch>('fetch', { sse: sseTemplatePath })
package/src/plugin.ts CHANGED
@@ -7,7 +7,7 @@ import { version } from '../package.json'
7
7
  import { pluginTsName } from '@kubb/plugin-ts'
8
8
  import { pluginZodName } from '@kubb/plugin-zod'
9
9
  import { clientGenerator } from './generators/clientGenerator.tsx'
10
- import { fetchClientTemplatePath, fetchSerializersTemplatePath, standardSchemaTemplatePath } from './templates.ts'
10
+ import { fetchClientTemplatePath, coreTemplatePath, serializersTemplatePath, sseTemplatePath, standardSchemaTemplatePath } from './templates.ts'
11
11
  import type { PluginFetch, ResolvedOptions, ResolverClient } from './types.ts'
12
12
 
13
13
  /**
@@ -79,22 +79,26 @@ export const pluginFetch = defineExtension<PluginFetch>({
79
79
 
80
80
  // `sdk` swaps the per-operation functions for the class-based SDK; left unset, the standalone
81
81
  // functions (which query plugins consume) stay.
82
- addGenerator(resolved.sdk ? createSdkGenerator<PluginFetch>() : clientGenerator)
82
+ addGenerator(resolved.sdk ? createSdkGenerator<PluginFetch>({ sse: sseTemplatePath }) : clientGenerator)
83
83
 
84
84
  const root = path.resolve(config.root, config.output.path)
85
- const baseURLExpression = baseURL ? (baseURL.includes('${') ? `\`${baseURL.replaceAll('`', '\\`')}\`` : JSON.stringify(baseURL)) : undefined
86
85
 
87
86
  injectFile({
88
87
  baseName: 'serializers.ts',
89
88
  path: path.resolve(root, '.kubb/serializers.ts'),
90
- copy: fetchSerializersTemplatePath,
89
+ copy: serializersTemplatePath,
90
+ })
91
+
92
+ injectFile({
93
+ baseName: 'core.ts',
94
+ path: path.resolve(root, '.kubb/core.ts'),
95
+ copy: coreTemplatePath,
91
96
  })
92
97
 
93
98
  injectFile({
94
99
  baseName: 'client.ts',
95
100
  path: path.resolve(root, '.kubb/client.ts'),
96
101
  copy: fetchClientTemplatePath,
97
- footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : undefined,
98
102
  })
99
103
 
100
104
  injectFile({
package/src/templates.ts CHANGED
@@ -1,13 +1,16 @@
1
+ import { createRequire } from 'node:module'
1
2
  import { fileURLToPath } from 'node:url'
2
3
 
3
- /** Absolute path to the fetch client template, copied into `.kubb/client.ts`. */
4
- export const fetchClientTemplatePath = fileURLToPath(new URL('../templates/fetch.ts', import.meta.url))
4
+ const require = createRequire(import.meta.url)
5
5
 
6
- /** Absolute path to the fetch serializers template, copied into `.kubb/serializers.ts`. */
7
- export const fetchSerializersTemplatePath = fileURLToPath(new URL('../templates/serializers.ts', import.meta.url))
6
+ /** Absolute path to the fetch client template, copied into `.kubb/client.ts`. */
7
+ export const fetchClientTemplatePath = fileURLToPath(new URL('../templates/client.ts', import.meta.url))
8
8
 
9
9
  /**
10
- * Absolute path to the Standard Schema runtime template. Pass it to a file node's `copy` field to
11
- * emit the helper into the generated `.kubb/standardSchema.ts` verbatim.
10
+ * Absolute paths to the runtime templates `@kubb/plugin-client` ships for every client plugin, copied into `.kubb/core.ts`,
11
+ * `.kubb/serializers.ts`, `.kubb/standardSchema.ts`, and `.kubb/sse.ts`.
12
12
  */
13
- export const standardSchemaTemplatePath = fileURLToPath(new URL('../templates/standardSchema.ts', import.meta.url))
13
+ export const coreTemplatePath = require.resolve('@kubb/plugin-client/templates/core.ts')
14
+ export const serializersTemplatePath = require.resolve('@kubb/plugin-client/templates/serializers.ts')
15
+ export const standardSchemaTemplatePath = require.resolve('@kubb/plugin-client/templates/standardSchema.ts')
16
+ export const sseTemplatePath = require.resolve('@kubb/plugin-client/templates/sse.ts')
@@ -0,0 +1,111 @@
1
+ import { createClientCore, getResponseContentType, type ResponseType, type Transport, type TransportResult } from './core.ts'
2
+ import { isJsonMediaType } from './serializers.ts'
3
+ import * as core from './core.ts'
4
+
5
+ export * from './core.ts'
6
+
7
+ /** The request and response objects of this transport, carried on every result and error. */
8
+ export type NativeRequest = Request
9
+ export type NativeResponse = Response
10
+
11
+ /**
12
+ * Extra `fetch` init the transport spreads onto every `Request`, an escape hatch for fields the runtime does not set itself such as `cache`, `redirect`, and Next.js's `next`.
13
+ */
14
+ export type FetchOptions = RequestInit & { next?: Record<string, unknown> }
15
+
16
+ export type RequestConfig<TBody = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = core.RequestConfig<TBody, TRequest, TResponse> & {
17
+ options?: FetchOptions
18
+ }
19
+ export type Options<TData extends core.DataShape, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = core.Options<
20
+ TData,
21
+ ThrowOnError,
22
+ TRequest,
23
+ TResponse
24
+ > & { options?: FetchOptions }
25
+ export type ClientConfig<TRequest = NativeRequest, TResponse = NativeResponse> = core.ClientConfig<TRequest, TResponse> & { options?: FetchOptions }
26
+ export type ResolvedRequest = core.ResolvedRequest & { options?: FetchOptions }
27
+ export type ResponseError<TError = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = core.ResponseError<TError, TRequest, TResponse>
28
+ /** The core's `ResponseError` class, typed with this transport's request and response. `instanceof` and `ResponseError.is` both match what the client throws. */
29
+ export const ResponseError = core.ResponseError as {
30
+ new <TError = unknown, TRequest = NativeRequest, TResponse = NativeResponse>(
31
+ config: ConstructorParameters<typeof core.ResponseError<TError, TRequest, TResponse>>[0],
32
+ ): core.ResponseError<TError, TRequest, TResponse>
33
+ is: typeof core.ResponseError.is
34
+ // `instanceof` narrows to this type, so it carries the native request and response.
35
+ prototype: core.ResponseError<unknown, NativeRequest, NativeResponse>
36
+ }
37
+
38
+ function detectResponseType(contentType: string | null): ResponseType | undefined {
39
+ if (!contentType) return undefined
40
+ if (contentType.includes('text/event-stream')) return 'stream'
41
+ if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
42
+ if (contentType.includes('text/')) return 'text'
43
+ if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
44
+ return undefined
45
+ }
46
+
47
+ async function parseResponse(response: Response, responseType?: ResponseType): Promise<unknown> {
48
+ if (response.status === 204 || response.status === 205 || response.status === 304 || !response.body) {
49
+ return undefined
50
+ }
51
+
52
+ const contentType = response.headers.get('Content-Type')
53
+ // A server may answer a streamed operation with JSON, or with an error body: both are decoded by their content type. Any other streamed success body stays a stream.
54
+ const detected = responseType === 'stream' && (!response.ok || isJsonMediaType(contentType)) ? undefined : responseType
55
+ switch (detected ?? detectResponseType(contentType)) {
56
+ case 'text':
57
+ case 'document':
58
+ return response.text()
59
+ case 'blob':
60
+ return response.blob()
61
+ case 'arraybuffer':
62
+ return response.arrayBuffer()
63
+ case 'stream':
64
+ return response.body ?? undefined
65
+ case 'json': {
66
+ // An empty body with a JSON content-type would make response.json() throw, so treat it as no data.
67
+ const body = await response.text()
68
+ return body ? JSON.parse(body) : undefined
69
+ }
70
+ }
71
+
72
+ const text = await response.text()
73
+ if (!text) return undefined
74
+ try {
75
+ return JSON.parse(text)
76
+ } catch {
77
+ return text
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The default transport that sends the resolved request through `globalThis.fetch` and returns the parsed body with the native request and response.
83
+ */
84
+ export const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
85
+ const init: RequestInit = {
86
+ ...request.options,
87
+ method: request.method,
88
+ headers: request.headers,
89
+ body: request.body,
90
+ signal: request.signal,
91
+ }
92
+ if (request.credentials) init.credentials = request.credentials
93
+
94
+ const nativeRequest = new Request(request.url, init)
95
+ const response = await globalThis.fetch(nativeRequest)
96
+ const data = await parseResponse(response, request.responseType)
97
+
98
+ return {
99
+ data,
100
+ status: response.status,
101
+ statusText: response.statusText,
102
+ headers: response.headers,
103
+ contentType: getResponseContentType(response.headers),
104
+ request: nativeRequest,
105
+ response,
106
+ }
107
+ }
108
+
109
+ export const client = createClientCore({ defaultTransport })
110
+
111
+ export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)
@@ -0,0 +1,2 @@
1
+ // Resolves `./core.ts` for the client template next to it. The generator copies the file from `@kubb/plugin-client` into `.kubb/core.ts`.
2
+ export * from '@kubb/plugin-client/templates/core.ts'