@kubb/plugin-axios 5.0.0-beta.77 → 5.0.0-beta.80

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,36 +1,56 @@
1
1
  import axios from 'axios'
2
2
  import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
3
+ import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, serializeCookies } from './serializers'
4
+ import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
5
+ import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
3
6
 
4
7
  /**
5
- * HTTP status codes treated as a success. A resolved call only ever carries a body from one of
6
- * these; everything else is an error (thrown by default, or surfaced on `error`).
8
+ * HTTP status codes treated as a success, everything else is an error.
7
9
  */
8
10
  export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
9
11
 
10
12
  /**
11
- * The success members of a per-status responses record (`{ '200': ...; '404': ... }`).
13
+ * The success members of a per-status responses record.
12
14
  */
13
15
  export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
14
16
 
15
17
  /**
16
- * The error members of a per-status responses record — every documented status that is not a 2xx.
18
+ * The error members of a per-status responses record, every documented status that is not a 2xx.
17
19
  */
18
20
  export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
19
21
 
20
22
  /**
21
- * Converts a response record's string status key to its numeric literal (`'200'` becomes `200`).
22
- * Non-numeric keys such as the OpenAPI `default` response stay `number`.
23
+ * Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`.
23
24
  */
24
25
  export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
25
26
 
26
27
  /**
27
- * One result variant for a single documented status. `status` is the numeric literal at the top
28
- * level, so a `switch (result.status)` narrows `data` (a 2xx status) or `error` (everything else) to
29
- * that status's payload.
28
+ * The plain body of a per-status response, unwrapping the `{ contentType; data }` union so an error result keeps the bare body union on `error`.
29
+ */
30
+ export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
31
+
32
+ /**
33
+ * The success variant for a single status, flattened so the negotiated `contentType` sits next to `data` and `switch (result.contentType)` narrows it.
34
+ */
35
+ export type SuccessVariant<TStatus, TEntry, TRequest, TResponse> = TEntry extends { contentType: string; data: unknown }
36
+ ? TEntry extends { contentType: infer TContentType; data: infer TData }
37
+ ? { status: ToStatusNumber<TStatus>; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse }
38
+ : never
39
+ : { status: ToStatusNumber<TStatus>; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
40
+
41
+ /**
42
+ * One result variant for a single documented status, keyed by the numeric `status` so a `switch (result.status)` narrows `data` or `error`.
30
43
  */
31
44
  export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
32
- ? { status: ToStatusNumber<TStatus>; data: TResponses[TStatus]; error: undefined; request: TRequest; response: TResponse }
33
- : { status: ToStatusNumber<TStatus>; data: undefined; error: TResponses[TStatus]; request: TRequest; response: TResponse }
45
+ ? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
46
+ : {
47
+ status: ToStatusNumber<TStatus>
48
+ data: undefined
49
+ error: DataOf<TResponses[TStatus]>
50
+ contentType: string | undefined
51
+ request: TRequest
52
+ response: TResponse
53
+ }
34
54
 
35
55
  /**
36
56
  * The union of every documented status' result variant.
@@ -40,60 +60,72 @@ export type ResultUnion<TResponses, TRequest, TResponse> = {
40
60
  }[keyof TResponses]
41
61
 
42
62
  /**
43
- * The union of just the success (2xx) status variants. Selected by status code, not by `error`, so an
44
- * untyped (`any`) error payload can never widen a success result's `data`.
63
+ * The union of just the success (2xx) status variants, selected by status code so an untyped error payload can never widen `data`.
45
64
  */
46
65
  export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
47
66
  [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
48
67
  }[Extract<keyof TResponses, SuccessStatusCode>]
49
68
 
50
69
  /**
51
- * The shape every generated function returns, discriminated by the top-level `status`. With
52
- * `throwOnError` (the default) a resolved call always means success, so the result is the union of the
53
- * 2xx variants and `error` is `undefined`; without it every documented status is a variant, so a
54
- * `switch (result.status)` (or narrowing on `error`) narrows `data` and `error` to that status's
55
- * payload. Operations with no typed responses fall back to a `status`/`request`/`response`-only result.
70
+ * The shape every generated function returns, discriminated by the top-level `status`, narrowing to the 2xx variants under `throwOnError` and to every documented status without it.
56
71
  */
57
72
  export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = ThrowOnError extends true
58
73
  ? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
59
- ? { status: number; data: SuccessOf<TResponses>; error: undefined; request: TRequest; response: TResponse }
74
+ ? {
75
+ status: number
76
+ data: SuccessOf<TResponses>
77
+ error: undefined
78
+ contentType: string | undefined
79
+ request: TRequest
80
+ response: TResponse
81
+ }
60
82
  : SuccessResultUnion<TResponses, TRequest, TResponse>
61
83
  : [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
62
- ? { status: number; data: undefined; error: undefined; request: TRequest; response: TResponse }
84
+ ? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
63
85
  : ResultUnion<TResponses, TRequest, TResponse>
64
86
 
65
87
  /**
66
- * The data-shaped keys of the grouped options object. `Options` subtracts these from the runtime
67
- * `RequestConfig` and adds them back, typed per operation, from the generated `<Name>Request` type.
88
+ * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
68
89
  */
69
- export type DataShape = { body?: unknown; headers?: unknown; path?: unknown; query?: unknown }
90
+ export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
70
91
 
71
- export type HeaderValue = string | number | boolean | null | undefined | object
72
- export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
73
92
  export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata'
74
93
 
75
94
  /**
76
- * Serializes the query object into a search string. Array and object members follow the configured
77
- * style (`form` with `explode` by default; `deepObject` for nested objects).
95
+ * Turns a raw response body into a parsed value, registered per media type as a codec's `deserialize` to handle formats the runtime does not decode itself.
96
+ */
97
+ export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
98
+
99
+ /**
100
+ * Serializes a request body for a single media type, registered per content type as a codec's `serialize` to encode formats the default serializer does not handle.
101
+ */
102
+ export type ContentBodySerializer = (body: unknown, contentType?: string) => unknown
103
+
104
+ /**
105
+ * A per-content-type codec registered on `codecs`, keyed by content type. `serialize` encodes the
106
+ * request body for that media type and `deserialize` decodes the response body. Either half is
107
+ * optional, so a codec can handle one direction.
78
108
  */
79
- export type QuerySerializer = (params: Record<string, unknown>) => string
109
+ export type Codec = {
110
+ serialize?: ContentBodySerializer
111
+ deserialize?: Deserializer
112
+ }
80
113
 
81
114
  /**
82
- * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`,
83
- * `ArrayBuffer`, and string bodies pass through untouched.
115
+ * The per-call content type selection, where a bare string sets the request content type and the object form also sets the response format sent as `Accept`.
84
116
  */
85
- export type BodySerializer = (body: unknown, contentType?: string) => unknown
117
+ export type ContentType = string | { request?: string; response?: string }
86
118
 
87
119
  /**
88
- * Parses a value before it is sent or after it is received, returning the parsed (and optionally
89
- * transformed) value. Wires zod parsing through the per-call `parser.request` / `parser.response` /
90
- * `parser.error` hooks (`error` runs on the error body when a non-2xx call does not throw).
120
+ * A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
121
+ * it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
122
+ * `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
123
+ * non-2xx call does not throw).
91
124
  */
92
- export type Parser<T = unknown> = (value: T) => T | Promise<T>
125
+ export type Validator<T = unknown> = StandardSchemaValidator<T>
93
126
 
94
127
  /**
95
- * A resolved security scheme carried on each generated call's `security` array. The runtime passes it
96
- * to the configured `auth` resolver and places the returned token accordingly.
128
+ * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
97
129
  */
98
130
  export type Auth = {
99
131
  type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
@@ -103,32 +135,22 @@ export type Auth = {
103
135
  }
104
136
 
105
137
  /**
106
- * The token a consumer returns for a scheme, or `undefined` to skip it. Bearer and basic schemes are
107
- * prefixed by the runtime (basic is base64-encoded), so return the raw token or `user:password`.
138
+ * The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
108
139
  */
109
140
  export type AuthToken = string | undefined
110
141
 
111
142
  /**
112
- * Resolves the token for a security scheme: either a static token used for every scheme, or a
113
- * callback called once per scheme on a guarded operation until one returns a token.
143
+ * Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token.
114
144
  */
115
145
  export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
116
146
 
117
147
  /**
118
- * Extra axios config the runtime spreads onto every request, an escape hatch for the per-call fields
119
- * it does not set itself, such as `timeout`, `proxy`, `maxRedirects`, `decompress`, and the
120
- * `onUploadProgress` / `onDownloadProgress` callbacks. The runtime-owned fields (`url`, `baseURL`,
121
- * `method`, `headers`, `params`, `paramsSerializer`, `data`, `transformRequest`, `signal`,
122
- * `responseType`, `validateStatus`) always win over anything set here. This is the per-request analog
123
- * of the `transport` instance, which stays the place for cross-cutting concerns like retries and
124
- * interceptors. It mirrors `options` in `@kubb/plugin-fetch`.
148
+ * Extra axios config the runtime spreads onto every request, an escape hatch for per-call fields it does not set itself such as `timeout`, `proxy`, and the progress callbacks.
125
149
  */
126
150
  export type AxiosOptions = AxiosRequestConfig
127
151
 
128
152
  /**
129
- * The request a generated function hands to the runtime. `body` / `headers` / `path` / `query` come
130
- * from the grouped options; everything else is plain request configuration. `transport` carries an
131
- * axios instance and `validateStatus` rides axios's own contract.
153
+ * The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
132
154
  */
133
155
  export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
134
156
  baseURL?: string
@@ -137,19 +159,21 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
137
159
  path?: Record<string, unknown>
138
160
  query?: unknown
139
161
  params?: unknown
162
+ cookies?: Record<string, unknown>
140
163
  body?: TBody
141
164
  headers?: HeadersInit
165
+ styles?: Styles
142
166
  signal?: AbortSignal
143
167
  options?: AxiosOptions
144
- contentType?: string
168
+ contentType?: ContentType
145
169
  responseType?: ResponseType
146
170
  throwOnError?: boolean
147
171
  validateStatus?: (status: number) => boolean
148
172
  client?: ClientInstance<TRequest, TResponse>
149
173
  transport?: AxiosInstance
150
- querySerializer?: QuerySerializer
151
- bodySerializer?: BodySerializer
152
- parser?: { request?: Parser; response?: Parser; error?: Parser }
174
+ serializer?: Serializers
175
+ codecs?: Record<string, Codec>
176
+ validator?: { request?: Validator; response?: Validator; error?: Validator }
153
177
  security?: Array<Auth>
154
178
  auth?: AuthResolver
155
179
  }
@@ -168,8 +192,7 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
168
192
  }
169
193
 
170
194
  /**
171
- * Client-level configuration shared by every call an instance makes. Per-call `RequestConfig`
172
- * overrides these. `transport` is the axios instance the client sends through.
195
+ * Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
173
196
  */
174
197
  export type ClientConfig = {
175
198
  baseURL?: string
@@ -178,8 +201,8 @@ export type ClientConfig = {
178
201
  throwOnError?: boolean
179
202
  validateStatus?: (status: number) => boolean
180
203
  transport?: AxiosInstance
181
- querySerializer?: QuerySerializer
182
- bodySerializer?: BodySerializer
204
+ serializer?: Serializers
205
+ codecs?: Record<string, Codec>
183
206
  auth?: AuthResolver
184
207
  }
185
208
 
@@ -190,18 +213,15 @@ export type CallResult<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>
190
213
  status: number
191
214
  data: unknown
192
215
  error: unknown
216
+ contentType: string | undefined
193
217
  request: TRequest
194
218
  response: TResponse
195
219
  }
196
220
 
197
- /**
198
- * An interceptor function for a channel value.
199
- */
200
221
  export type InterceptorFn<T> = (value: T) => T | Promise<T>
201
222
 
202
223
  /**
203
- * A single interceptor channel (request, response, or error) exposing the `use` / `eject` / `update`
204
- * API shared with the fetch and ky runtimes, backed by axios's native interceptor managers.
224
+ * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API, backed by axios's native interceptor managers.
205
225
  */
206
226
  export type InterceptorChannel<T> = {
207
227
  use: (fn: InterceptorFn<T>) => number
@@ -210,9 +230,7 @@ export type InterceptorChannel<T> = {
210
230
  }
211
231
 
212
232
  /**
213
- * The three interceptor channels every client instance exposes, wrapping axios's native
214
- * `interceptors.request` / `interceptors.response`. The `error` channel maps onto the response
215
- * manager's rejection handler.
233
+ * The three interceptor channels every client instance exposes, wrapping axios's native managers with `error` mapped onto the response rejection handler.
216
234
  */
217
235
  export type Interceptors = {
218
236
  request: InterceptorChannel<InternalAxiosRequestConfig>
@@ -234,97 +252,132 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
234
252
  }
235
253
 
236
254
  /**
237
- * Thrown for responses outside the 2xx range, so a resolved call always means success. The parsed
238
- * error body and the native request config / response stay reachable on the error.
255
+ * Thrown for a non-2xx response, so a resolved call always means success.
239
256
  */
240
- export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
241
- data: TError
242
- status: number
243
- statusText: string
244
- request: TRequest
245
- response: TResponse
257
+ /**
258
+ * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
259
+ */
260
+ export type ServerSentEvent<TData = unknown> = {
261
+ data: TData
262
+ event?: string
263
+ id?: string
264
+ retry?: number
265
+ }
246
266
 
247
- constructor(config: { data: TError; status: number; statusText: string; request: TRequest; response: TResponse }) {
248
- super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
249
- this.name = 'ResponseError'
250
- this.data = config.data
251
- this.status = config.status
252
- this.statusText = config.statusText
253
- this.request = config.request
254
- this.response = config.response
267
+ async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
268
+ if (!('getReader' in stream)) {
269
+ yield* stream
270
+ return
271
+ }
272
+
273
+ const reader = stream.getReader()
274
+ try {
275
+ while (true) {
276
+ const { done, value } = await reader.read()
277
+ if (done) return
278
+ yield value
279
+ }
280
+ } finally {
281
+ await reader.cancel().catch(() => {})
255
282
  }
256
283
  }
257
284
 
258
- export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
285
+ function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
286
+ const data: Array<string> = []
287
+ const event: ServerSentEvent<TData> = { data: undefined as TData }
288
+ let seen = false
289
+
290
+ for (const line of raw.split('\n')) {
291
+ if (!line || line.startsWith(':')) continue
292
+ seen = true
293
+ const index = line.indexOf(':')
294
+ const field = index === -1 ? line : line.slice(0, index)
295
+ const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
296
+ if (field === 'data') data.push(value)
297
+ else if (field === 'event') event.event = value
298
+ else if (field === 'id') event.id = value
299
+ else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
300
+ }
259
301
 
260
- function isFormBody(body: unknown): boolean {
261
- return (
262
- body instanceof FormData ||
263
- body instanceof URLSearchParams ||
264
- body instanceof Blob ||
265
- body instanceof ArrayBuffer ||
266
- ArrayBuffer.isView(body) ||
267
- typeof body === 'string'
268
- )
269
- }
302
+ if (!seen) return undefined
270
303
 
271
- function appendFormDataValue(formData: FormData, key: string, value: unknown): void {
272
- if (value === undefined || value === null) return
273
- if (value instanceof Blob) formData.append(key, value)
274
- else if (value instanceof Date) formData.append(key, value.toISOString())
275
- else if (typeof value === 'object') formData.append(key, JSON.stringify(value))
276
- else formData.append(key, String(value))
304
+ if (data.length) {
305
+ const joined = data.join('\n')
306
+ try {
307
+ event.data = JSON.parse(joined) as TData
308
+ } catch {
309
+ event.data = joined as TData
310
+ }
311
+ }
312
+ return event
277
313
  }
278
314
 
279
315
  /**
280
- * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
281
- * For `multipart/form-data` plain objects become `FormData` and for
282
- * `application/x-www-form-urlencoded` they become `URLSearchParams`.
316
+ * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
283
317
  */
284
- export const defaultBodySerializer: BodySerializer = (body, contentType) => {
285
- if (body === undefined || body === null) return undefined
286
- if (isFormBody(body)) return body
287
- if (contentType?.includes('multipart/form-data')) {
288
- const formData = new FormData()
289
- for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
290
- if (Array.isArray(value)) for (const item of value) appendFormDataValue(formData, key, item)
291
- else appendFormDataValue(formData, key, value)
318
+ export async function* parseEventStream<TData = unknown>(
319
+ stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
320
+ ): AsyncGenerator<ServerSentEvent<TData>> {
321
+ const decoder = new TextDecoder()
322
+ const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
323
+ let buffer = ''
324
+
325
+ for await (const chunk of readBytes(stream)) {
326
+ const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
327
+ buffer = blocks.pop() ?? ''
328
+ for (const block of blocks) {
329
+ const event = parseEvent<TData>(block)
330
+ if (event) yield event
292
331
  }
293
- return formData
294
- }
295
- if (contentType?.includes('application/x-www-form-urlencoded')) {
296
- return new URLSearchParams(body as Record<string, string>)
297
332
  }
298
- return JSON.stringify(body)
333
+
334
+ const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
335
+ if (event) yield event
299
336
  }
300
337
 
301
- function appendQueryValue(search: URLSearchParams, key: string, value: unknown): void {
302
- if (value === undefined || value === null) return
303
- if (Array.isArray(value)) {
304
- for (const item of value) appendQueryValue(search, key, item)
305
- return
306
- }
307
- if (typeof value === 'object') {
308
- for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
309
- appendQueryValue(search, `${key}[${prop}]`, propValue)
310
- }
311
- return
312
- }
313
- search.append(key, String(value))
338
+ /**
339
+ * The resolved shape returned by a generated `text/event-stream` operation: the typed event
340
+ * `stream` plus the native `response`.
341
+ */
342
+ export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
343
+ stream: AsyncGenerator<ServerSentEvent<TData>>
344
+ response: TResponse
314
345
  }
315
346
 
316
347
  /**
317
- * Default query serializer: arrays explode into repeated keys and nested objects use the
318
- * `deepObject` style (`key[prop]=value`).
348
+ * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
349
+ * the parsed events as a typed async iterator. Generated SSE operations call this.
319
350
  */
320
- export const defaultQuerySerializer: QuerySerializer = (params) => {
321
- const search = new URLSearchParams()
322
- for (const [key, value] of Object.entries(params)) {
323
- appendQueryValue(search, key, value)
351
+ export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
352
+ const { data, response } = await result
353
+ return {
354
+ response,
355
+ stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
356
+ }
357
+ }
358
+
359
+ export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
360
+ data: TError
361
+ status: number
362
+ statusText: string
363
+ contentType: string | undefined
364
+ request: TRequest
365
+ response: TResponse
366
+
367
+ constructor(config: { data: TError; status: number; statusText: string; contentType?: string; request: TRequest; response: TResponse }) {
368
+ super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
369
+ this.name = 'ResponseError'
370
+ this.data = config.data
371
+ this.status = config.status
372
+ this.statusText = config.statusText
373
+ this.contentType = config.contentType
374
+ this.request = config.request
375
+ this.response = config.response
324
376
  }
325
- return search.toString()
326
377
  }
327
378
 
379
+ export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
380
+
328
381
  function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
329
382
  if (!headers) return {}
330
383
  const entries = Array.isArray(headers) ? headers : Object.entries(headers)
@@ -341,23 +394,30 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
341
394
  }
342
395
 
343
396
  /**
344
- * Joins the base and request URL parts, interpolates `{param}` segments from the path params
345
- * (URL-encoded), and appends the serialized query. Backs `getUrl` so a URL can be constructed
346
- * without sending the request.
397
+ * Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, backing `getUrl`.
347
398
  */
348
- function serializeUrl(parts: Array<string | undefined>, pathParams: Record<string, unknown>, search: string): string {
399
+ function serializeUrl({
400
+ parts,
401
+ pathParams,
402
+ search,
403
+ pathSerializer = defaultPathSerializer,
404
+ pathStyles,
405
+ }: {
406
+ parts: Array<string | undefined>
407
+ pathParams: Record<string, unknown>
408
+ search: string
409
+ pathSerializer?: PathSerializer
410
+ pathStyles?: Record<string, PathParamStyle>
411
+ }): string {
349
412
  const path = parts
350
413
  .filter(Boolean)
351
414
  .join('')
352
- .replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
415
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
353
416
  return path + (search ? `?${search}` : '')
354
417
  }
355
418
 
356
419
  /**
357
- * Walks the per-operation security in order and places the first resolved token on the request,
358
- * mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
359
- * Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
360
- * `name` in the header, query, or cookie.
420
+ * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
361
421
  */
362
422
  export async function resolveAuth(params: {
363
423
  security: Array<Auth> | undefined
@@ -384,15 +444,13 @@ export async function resolveAuth(params: {
384
444
  }
385
445
  }
386
446
 
387
- async function runParser<T>(parser: Parser | undefined, value: T): Promise<T> {
388
- if (!parser) return value
389
- return (await parser(value)) as T
447
+ async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
448
+ if (!validator) return value
449
+ return validateStandardSchema(validator, value)
390
450
  }
391
451
 
392
452
  /**
393
- * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API. A stable
394
- * external id is mapped onto axios's own id so `update` can swap a handler in place even though axios
395
- * has no native update.
453
+ * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
396
454
  */
397
455
  function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
398
456
  const ids = new Map<number, number>()
@@ -418,13 +476,32 @@ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number,
418
476
  }
419
477
 
420
478
  /**
421
- * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`). The
422
- * interceptor channels delegate to the instance's native managers, and `querySerializer` /
423
- * `bodySerializer` map onto `paramsSerializer` / `transformRequest`.
424
- *
425
- * `throwOnError` rides `validateStatus`. With it on, axios rejects a non-2xx status and the runtime
426
- * normalizes the error to `ResponseError`. With it off, an internal `validateStatus: () => true`
427
- * resolves every status into `{ data, error, request, response }`.
479
+ * The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
480
+ */
481
+ function baseContentType(value: string | null | undefined): string | undefined {
482
+ if (!value) return undefined
483
+ return value.split(';')[0]!.trim().toLowerCase() || undefined
484
+ }
485
+
486
+ /**
487
+ * Reads the negotiated response content type from the response headers as a base media type.
488
+ */
489
+ function getResponseContentType(headers: Record<string, unknown> | undefined): string | undefined {
490
+ if (!headers) return undefined
491
+ const value = headers['content-type'] ?? headers['Content-Type']
492
+ return baseContentType(typeof value === 'string' ? value : undefined)
493
+ }
494
+
495
+ /**
496
+ * Normalizes the `contentType` option to its `{ request, response }` form, treating a bare string as the request content type.
497
+ */
498
+ function resolveContentType(contentType: ContentType | undefined): { request?: string; response?: string } {
499
+ if (typeof contentType === 'string') return { request: contentType }
500
+ return contentType ?? {}
501
+ }
502
+
503
+ /**
504
+ * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`), with `throwOnError` riding axios's `validateStatus`.
428
505
  */
429
506
  export function createClientCore<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>(options: ClientConfig = {}): ClientInstance<TRequest, TResponse> {
430
507
  let config: ClientConfig = { ...options }
@@ -453,11 +530,17 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
453
530
 
454
531
  const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
455
532
  const activeInstance = requestConfig.transport ?? config.transport ?? instance
456
- const querySerializer = requestConfig.querySerializer ?? config.querySerializer ?? defaultQuerySerializer
457
- const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
458
-
459
- const headers = mergeHeaders(config.headers, requestConfig.headers)
460
- const requestContentType = requestConfig.contentType ?? headers['Content-Type'] ?? headers['content-type']
533
+ const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
534
+ const bodySerializer = requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer
535
+ const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
536
+ const codecs = { ...config.codecs, ...requestConfig.codecs }
537
+
538
+ const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
539
+ const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
540
+ const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
541
+ if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
542
+ headers['Accept'] = responseContentType
543
+ }
461
544
 
462
545
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
463
546
 
@@ -468,17 +551,28 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
468
551
  query,
469
552
  })
470
553
 
471
- const validatedBody = await runParser(requestConfig.parser?.request, requestConfig.body)
472
- const body = bodySerializer(validatedBody, requestContentType)
554
+ if (requestConfig.cookies) {
555
+ const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
556
+ if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
557
+ }
558
+
559
+ const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
560
+ const requestContentTypeBase = baseContentType(requestContentType)
561
+ const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
562
+ const body = contentCodec?.serialize
563
+ ? contentCodec.serialize(validatedBody, requestContentType)
564
+ : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
473
565
  // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
474
566
  if (body instanceof FormData) {
475
567
  delete headers['Content-Type']
476
568
  delete headers['content-type']
477
- } else if (requestConfig.contentType) {
478
- headers['Content-Type'] = requestConfig.contentType
569
+ } else if (requestContentTypeOption) {
570
+ headers['Content-Type'] = requestContentTypeOption
479
571
  }
480
572
  const pathParams = requestConfig.path ?? {}
481
- const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
573
+ const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
574
+ pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
575
+ )
482
576
  const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
483
577
 
484
578
  const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
@@ -493,7 +587,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
493
587
  method: requestConfig.method ?? 'GET',
494
588
  headers,
495
589
  params: query,
496
- paramsSerializer: (params) => querySerializer(params as Record<string, unknown>),
590
+ paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
497
591
  data: body,
498
592
  transformRequest: (data) => data,
499
593
  signal: requestConfig.signal,
@@ -501,15 +595,28 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
501
595
  validateStatus,
502
596
  }
503
597
 
598
+ // Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser;
599
+ // the default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
600
+ if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
601
+ axiosConfig.adapter = 'fetch'
602
+ }
603
+
504
604
  try {
505
605
  const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
506
606
  const isSuccess = response.status >= 200 && response.status < 300
507
- const data = isSuccess ? await runParser(requestConfig.parser?.response, response.data) : undefined
508
- const error = isSuccess ? undefined : await runParser(requestConfig.parser?.error, response.data)
607
+ const contentType = getResponseContentType(response.headers as Record<string, unknown>)
608
+ let decoded: unknown = response.data
609
+ if (contentType) {
610
+ const codec = codecs[contentType]
611
+ if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
612
+ }
613
+ const data = isSuccess ? await runValidator(requestConfig.validator?.response, decoded) : undefined
614
+ const error = isSuccess ? undefined : await runValidator(requestConfig.validator?.error, decoded)
509
615
  return {
510
616
  status: response.status,
511
617
  data,
512
618
  error,
619
+ contentType,
513
620
  request: response.config as TRequest,
514
621
  response: response as TResponse,
515
622
  }
@@ -520,6 +627,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
520
627
  data: axiosError.response.data,
521
628
  status: axiosError.response.status,
522
629
  statusText: axiosError.response.statusText,
630
+ contentType: getResponseContentType(axiosError.response.headers as Record<string, unknown>),
523
631
  request: axiosError.config as TRequest,
524
632
  response: axiosError.response as TResponse,
525
633
  })
@@ -534,9 +642,16 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
534
642
  return config
535
643
  }
536
644
  client.getUrl = (requestConfig) => {
537
- const querySerializer = requestConfig.querySerializer ?? config.querySerializer ?? defaultQuerySerializer
645
+ const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
646
+ const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
538
647
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
539
- return serializeUrl([config.baseURL, requestConfig.baseURL, requestConfig.url], requestConfig.path ?? {}, querySerializer(query))
648
+ return serializeUrl({
649
+ parts: [config.baseURL, requestConfig.baseURL, requestConfig.url],
650
+ pathParams: requestConfig.path ?? {},
651
+ search: querySerializer(query, requestConfig.styles?.query),
652
+ pathSerializer,
653
+ pathStyles: requestConfig.styles?.path,
654
+ })
540
655
  }
541
656
  client.interceptors = interceptors
542
657
  client.createClient = (next) => createClientCore<TRequest, TResponse>({ ...config, ...next })