@kubb/plugin-axios 5.0.0-beta.76 → 5.0.0-beta.79

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.
78
101
  */
79
- export type QuerySerializer = (params: Record<string, unknown>) => string
102
+ export type ContentBodySerializer = (body: unknown, contentType?: string) => unknown
80
103
 
81
104
  /**
82
- * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`,
83
- * `ArrayBuffer`, and string bodies pass through untouched.
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.
84
108
  */
85
- export type BodySerializer = (body: unknown, contentType?: string) => unknown
109
+ export type Codec = {
110
+ serialize?: ContentBodySerializer
111
+ deserialize?: Deserializer
112
+ }
86
113
 
87
114
  /**
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).
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`.
91
116
  */
92
- export type Parser<T = unknown> = (value: T) => T | Promise<T>
117
+ export type ContentType = string | { request?: string; response?: string }
93
118
 
94
119
  /**
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.
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).
124
+ */
125
+ export type Validator<T = unknown> = StandardSchemaValidator<T>
126
+
127
+ /**
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,21 +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
- * The request a generated function hands to the runtime. `body` / `headers` / `path` / `query` come
119
- * from the grouped options; everything else is plain request configuration. `transport` carries an
120
- * axios instance and `validateStatus` rides axios's own contract.
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.
149
+ */
150
+ export type AxiosOptions = AxiosRequestConfig
151
+
152
+ /**
153
+ * The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
121
154
  */
122
155
  export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
123
156
  baseURL?: string
@@ -126,18 +159,21 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
126
159
  path?: Record<string, unknown>
127
160
  query?: unknown
128
161
  params?: unknown
162
+ cookies?: Record<string, unknown>
129
163
  body?: TBody
130
164
  headers?: HeadersInit
165
+ styles?: Styles
131
166
  signal?: AbortSignal
132
- contentType?: string
167
+ options?: AxiosOptions
168
+ contentType?: ContentType
133
169
  responseType?: ResponseType
134
170
  throwOnError?: boolean
135
171
  validateStatus?: (status: number) => boolean
136
172
  client?: ClientInstance<TRequest, TResponse>
137
173
  transport?: AxiosInstance
138
- querySerializer?: QuerySerializer
139
- bodySerializer?: BodySerializer
140
- parser?: { request?: Parser; response?: Parser; error?: Parser }
174
+ serializer?: Serializers
175
+ codecs?: Record<string, Codec>
176
+ validator?: { request?: Validator; response?: Validator; error?: Validator }
141
177
  security?: Array<Auth>
142
178
  auth?: AuthResolver
143
179
  }
@@ -156,17 +192,17 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
156
192
  }
157
193
 
158
194
  /**
159
- * Client-level configuration shared by every call an instance makes. Per-call `RequestConfig`
160
- * 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`.
161
196
  */
162
197
  export type ClientConfig = {
163
198
  baseURL?: string
164
199
  headers?: HeadersInit
200
+ options?: AxiosOptions
165
201
  throwOnError?: boolean
166
202
  validateStatus?: (status: number) => boolean
167
203
  transport?: AxiosInstance
168
- querySerializer?: QuerySerializer
169
- bodySerializer?: BodySerializer
204
+ serializer?: Serializers
205
+ codecs?: Record<string, Codec>
170
206
  auth?: AuthResolver
171
207
  }
172
208
 
@@ -177,18 +213,15 @@ export type CallResult<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>
177
213
  status: number
178
214
  data: unknown
179
215
  error: unknown
216
+ contentType: string | undefined
180
217
  request: TRequest
181
218
  response: TResponse
182
219
  }
183
220
 
184
- /**
185
- * An interceptor function for a channel value.
186
- */
187
221
  export type InterceptorFn<T> = (value: T) => T | Promise<T>
188
222
 
189
223
  /**
190
- * A single interceptor channel (request, response, or error) exposing the `use` / `eject` / `update`
191
- * 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.
192
225
  */
193
226
  export type InterceptorChannel<T> = {
194
227
  use: (fn: InterceptorFn<T>) => number
@@ -197,9 +230,7 @@ export type InterceptorChannel<T> = {
197
230
  }
198
231
 
199
232
  /**
200
- * The three interceptor channels every client instance exposes, wrapping axios's native
201
- * `interceptors.request` / `interceptors.response`. The `error` channel maps onto the response
202
- * 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.
203
234
  */
204
235
  export type Interceptors = {
205
236
  request: InterceptorChannel<InternalAxiosRequestConfig>
@@ -221,97 +252,132 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
221
252
  }
222
253
 
223
254
  /**
224
- * Thrown for responses outside the 2xx range, so a resolved call always means success. The parsed
225
- * 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.
226
256
  */
227
- export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
228
- data: TError
229
- status: number
230
- statusText: string
231
- request: TRequest
232
- 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
+ }
233
266
 
234
- constructor(config: { data: TError; status: number; statusText: string; request: TRequest; response: TResponse }) {
235
- super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
236
- this.name = 'ResponseError'
237
- this.data = config.data
238
- this.status = config.status
239
- this.statusText = config.statusText
240
- this.request = config.request
241
- 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(() => {})
242
282
  }
243
283
  }
244
284
 
245
- 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
+ }
246
301
 
247
- function isFormBody(body: unknown): boolean {
248
- return (
249
- body instanceof FormData ||
250
- body instanceof URLSearchParams ||
251
- body instanceof Blob ||
252
- body instanceof ArrayBuffer ||
253
- ArrayBuffer.isView(body) ||
254
- typeof body === 'string'
255
- )
256
- }
302
+ if (!seen) return undefined
257
303
 
258
- function appendFormDataValue(formData: FormData, key: string, value: unknown): void {
259
- if (value === undefined || value === null) return
260
- if (value instanceof Blob) formData.append(key, value)
261
- else if (value instanceof Date) formData.append(key, value.toISOString())
262
- else if (typeof value === 'object') formData.append(key, JSON.stringify(value))
263
- 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
264
313
  }
265
314
 
266
315
  /**
267
- * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
268
- * For `multipart/form-data` plain objects become `FormData` and for
269
- * `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.
270
317
  */
271
- export const defaultBodySerializer: BodySerializer = (body, contentType) => {
272
- if (body === undefined || body === null) return undefined
273
- if (isFormBody(body)) return body
274
- if (contentType?.includes('multipart/form-data')) {
275
- const formData = new FormData()
276
- for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
277
- if (Array.isArray(value)) for (const item of value) appendFormDataValue(formData, key, item)
278
- 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
279
331
  }
280
- return formData
281
332
  }
282
- if (contentType?.includes('application/x-www-form-urlencoded')) {
283
- return new URLSearchParams(body as Record<string, string>)
284
- }
285
- return JSON.stringify(body)
333
+
334
+ const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
335
+ if (event) yield event
286
336
  }
287
337
 
288
- function appendQueryValue(search: URLSearchParams, key: string, value: unknown): void {
289
- if (value === undefined || value === null) return
290
- if (Array.isArray(value)) {
291
- for (const item of value) appendQueryValue(search, key, item)
292
- return
293
- }
294
- if (typeof value === 'object') {
295
- for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
296
- appendQueryValue(search, `${key}[${prop}]`, propValue)
297
- }
298
- return
299
- }
300
- 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
301
345
  }
302
346
 
303
347
  /**
304
- * Default query serializer: arrays explode into repeated keys and nested objects use the
305
- * `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.
306
350
  */
307
- export const defaultQuerySerializer: QuerySerializer = (params) => {
308
- const search = new URLSearchParams()
309
- for (const [key, value] of Object.entries(params)) {
310
- 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
311
376
  }
312
- return search.toString()
313
377
  }
314
378
 
379
+ export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
380
+
315
381
  function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
316
382
  if (!headers) return {}
317
383
  const entries = Array.isArray(headers) ? headers : Object.entries(headers)
@@ -328,23 +394,30 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
328
394
  }
329
395
 
330
396
  /**
331
- * Joins the base and request URL parts, interpolates `{param}` segments from the path params
332
- * (URL-encoded), and appends the serialized query. Backs `getUrl` so a URL can be constructed
333
- * without sending the request.
397
+ * Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, backing `getUrl`.
334
398
  */
335
- 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 {
336
412
  const path = parts
337
413
  .filter(Boolean)
338
414
  .join('')
339
- .replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
415
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
340
416
  return path + (search ? `?${search}` : '')
341
417
  }
342
418
 
343
419
  /**
344
- * Walks the per-operation security in order and places the first resolved token on the request,
345
- * mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
346
- * Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
347
- * `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.
348
421
  */
349
422
  export async function resolveAuth(params: {
350
423
  security: Array<Auth> | undefined
@@ -371,15 +444,13 @@ export async function resolveAuth(params: {
371
444
  }
372
445
  }
373
446
 
374
- async function runParser<T>(parser: Parser | undefined, value: T): Promise<T> {
375
- if (!parser) return value
376
- 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)
377
450
  }
378
451
 
379
452
  /**
380
- * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API. A stable
381
- * external id is mapped onto axios's own id so `update` can swap a handler in place even though axios
382
- * 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.
383
454
  */
384
455
  function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
385
456
  const ids = new Map<number, number>()
@@ -405,13 +476,32 @@ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number,
405
476
  }
406
477
 
407
478
  /**
408
- * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`). The
409
- * interceptor channels delegate to the instance's native managers, and `querySerializer` /
410
- * `bodySerializer` map onto `paramsSerializer` / `transformRequest`.
411
- *
412
- * `throwOnError` rides `validateStatus`. With it on, axios rejects a non-2xx status and the runtime
413
- * normalizes the error to `ResponseError`. With it off, an internal `validateStatus: () => true`
414
- * 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`.
415
505
  */
416
506
  export function createClientCore<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>(options: ClientConfig = {}): ClientInstance<TRequest, TResponse> {
417
507
  let config: ClientConfig = { ...options }
@@ -440,11 +530,17 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
440
530
 
441
531
  const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
442
532
  const activeInstance = requestConfig.transport ?? config.transport ?? instance
443
- const querySerializer = requestConfig.querySerializer ?? config.querySerializer ?? defaultQuerySerializer
444
- const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
445
-
446
- const headers = mergeHeaders(config.headers, requestConfig.headers)
447
- 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
+ }
448
544
 
449
545
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
450
546
 
@@ -455,29 +551,43 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
455
551
  query,
456
552
  })
457
553
 
458
- const validatedBody = await runParser(requestConfig.parser?.request, requestConfig.body)
459
- 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 })
460
565
  // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
461
566
  if (body instanceof FormData) {
462
567
  delete headers['Content-Type']
463
568
  delete headers['content-type']
464
- } else if (requestConfig.contentType) {
465
- headers['Content-Type'] = requestConfig.contentType
569
+ } else if (requestContentTypeOption) {
570
+ headers['Content-Type'] = requestContentTypeOption
466
571
  }
467
572
  const pathParams = requestConfig.path ?? {}
468
- 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
+ )
469
576
  const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
470
577
 
471
578
  const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
472
579
  const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
473
580
 
581
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
582
+
474
583
  const axiosConfig: AxiosRequestConfig = {
584
+ ...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
475
585
  url,
476
586
  baseURL,
477
587
  method: requestConfig.method ?? 'GET',
478
588
  headers,
479
589
  params: query,
480
- paramsSerializer: (params) => querySerializer(params as Record<string, unknown>),
590
+ paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
481
591
  data: body,
482
592
  transformRequest: (data) => data,
483
593
  signal: requestConfig.signal,
@@ -485,15 +595,28 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
485
595
  validateStatus,
486
596
  }
487
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
+
488
604
  try {
489
605
  const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
490
606
  const isSuccess = response.status >= 200 && response.status < 300
491
- const data = isSuccess ? await runParser(requestConfig.parser?.response, response.data) : undefined
492
- 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)
493
615
  return {
494
616
  status: response.status,
495
617
  data,
496
618
  error,
619
+ contentType,
497
620
  request: response.config as TRequest,
498
621
  response: response as TResponse,
499
622
  }
@@ -504,6 +627,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
504
627
  data: axiosError.response.data,
505
628
  status: axiosError.response.status,
506
629
  statusText: axiosError.response.statusText,
630
+ contentType: getResponseContentType(axiosError.response.headers as Record<string, unknown>),
507
631
  request: axiosError.config as TRequest,
508
632
  response: axiosError.response as TResponse,
509
633
  })
@@ -518,9 +642,16 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
518
642
  return config
519
643
  }
520
644
  client.getUrl = (requestConfig) => {
521
- 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
522
647
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
523
- 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
+ })
524
655
  }
525
656
  client.interceptors = interceptors
526
657
  client.createClient = (next) => createClientCore<TRequest, TResponse>({ ...config, ...next })