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

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.
@@ -86,7 +86,8 @@ export type BodySerializer = (body: unknown, contentType?: string) => unknown
86
86
 
87
87
  /**
88
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` hooks.
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).
90
91
  */
91
92
  export type Parser<T = unknown> = (value: T) => T | Promise<T>
92
93
 
@@ -113,6 +114,17 @@ export type AuthToken = string | undefined
113
114
  */
114
115
  export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
115
116
 
117
+ /**
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`.
125
+ */
126
+ export type AxiosOptions = AxiosRequestConfig
127
+
116
128
  /**
117
129
  * The request a generated function hands to the runtime. `body` / `headers` / `path` / `query` come
118
130
  * from the grouped options; everything else is plain request configuration. `transport` carries an
@@ -128,6 +140,7 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
128
140
  body?: TBody
129
141
  headers?: HeadersInit
130
142
  signal?: AbortSignal
143
+ options?: AxiosOptions
131
144
  contentType?: string
132
145
  responseType?: ResponseType
133
146
  throwOnError?: boolean
@@ -136,7 +149,7 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
136
149
  transport?: AxiosInstance
137
150
  querySerializer?: QuerySerializer
138
151
  bodySerializer?: BodySerializer
139
- parser?: { request?: Parser; response?: Parser }
152
+ parser?: { request?: Parser; response?: Parser; error?: Parser }
140
153
  security?: Array<Auth>
141
154
  auth?: AuthResolver
142
155
  }
@@ -161,6 +174,7 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
161
174
  export type ClientConfig = {
162
175
  baseURL?: string
163
176
  headers?: HeadersInit
177
+ options?: AxiosOptions
164
178
  throwOnError?: boolean
165
179
  validateStatus?: (status: number) => boolean
166
180
  transport?: AxiosInstance
@@ -254,13 +268,30 @@ function isFormBody(body: unknown): boolean {
254
268
  )
255
269
  }
256
270
 
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))
277
+ }
278
+
257
279
  /**
258
280
  * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
259
- * For `application/x-www-form-urlencoded` plain objects become `URLSearchParams`.
281
+ * For `multipart/form-data` plain objects become `FormData` and for
282
+ * `application/x-www-form-urlencoded` they become `URLSearchParams`.
260
283
  */
261
284
  export const defaultBodySerializer: BodySerializer = (body, contentType) => {
262
285
  if (body === undefined || body === null) return undefined
263
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)
292
+ }
293
+ return formData
294
+ }
264
295
  if (contentType?.includes('application/x-www-form-urlencoded')) {
265
296
  return new URLSearchParams(body as Record<string, string>)
266
297
  }
@@ -426,10 +457,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
426
457
  const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
427
458
 
428
459
  const headers = mergeHeaders(config.headers, requestConfig.headers)
429
- if (requestConfig.contentType && requestConfig.contentType !== 'multipart/form-data') {
430
- headers['Content-Type'] = requestConfig.contentType
431
- }
432
- const contentType = headers['Content-Type'] ?? headers['content-type']
460
+ const requestContentType = requestConfig.contentType ?? headers['Content-Type'] ?? headers['content-type']
433
461
 
434
462
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
435
463
 
@@ -441,6 +469,14 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
441
469
  })
442
470
 
443
471
  const validatedBody = await runParser(requestConfig.parser?.request, requestConfig.body)
472
+ const body = bodySerializer(validatedBody, requestContentType)
473
+ // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
474
+ if (body instanceof FormData) {
475
+ delete headers['Content-Type']
476
+ delete headers['content-type']
477
+ } else if (requestConfig.contentType) {
478
+ headers['Content-Type'] = requestConfig.contentType
479
+ }
444
480
  const pathParams = requestConfig.path ?? {}
445
481
  const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
446
482
  const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
@@ -448,15 +484,18 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
448
484
  const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
449
485
  const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
450
486
 
487
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
488
+
451
489
  const axiosConfig: AxiosRequestConfig = {
490
+ ...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
452
491
  url,
453
492
  baseURL,
454
493
  method: requestConfig.method ?? 'GET',
455
494
  headers,
456
495
  params: query,
457
496
  paramsSerializer: (params) => querySerializer(params as Record<string, unknown>),
458
- data: validatedBody,
459
- transformRequest: (data) => bodySerializer(data, contentType),
497
+ data: body,
498
+ transformRequest: (data) => data,
460
499
  signal: requestConfig.signal,
461
500
  responseType: requestConfig.responseType,
462
501
  validateStatus,
@@ -466,10 +505,11 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
466
505
  const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
467
506
  const isSuccess = response.status >= 200 && response.status < 300
468
507
  const data = isSuccess ? await runParser(requestConfig.parser?.response, response.data) : undefined
508
+ const error = isSuccess ? undefined : await runParser(requestConfig.parser?.error, response.data)
469
509
  return {
470
510
  status: response.status,
471
511
  data,
472
- error: isSuccess ? undefined : response.data,
512
+ error,
473
513
  request: response.config as TRequest,
474
514
  response: response as TResponse,
475
515
  }