@kubb/plugin-fetch 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.
@@ -84,7 +84,8 @@ export type BodySerializer = (body: unknown, contentType?: string) => BodyInit |
84
84
 
85
85
  /**
86
86
  * Parses a value before it is sent or after it is received, returning the parsed (and optionally
87
- * transformed) value. Wires zod parsing through the per-call `parser.request` / `parser.response` hooks.
87
+ * transformed) value. Wires zod parsing through the per-call `parser.request` / `parser.response` /
88
+ * `parser.error` hooks (`error` runs on the error body when a non-2xx call does not throw).
88
89
  */
89
90
  export type Parser<T = unknown> = (value: T) => T | Promise<T>
90
91
 
@@ -111,6 +112,14 @@ export type AuthToken = string | undefined
111
112
  */
112
113
  export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
113
114
 
115
+ /**
116
+ * Extra `fetch` init the transport spreads onto every `Request`, an escape hatch for the fields the
117
+ * runtime does not set itself: `cache`, `mode`, `redirect`, `keepalive`, `duplex`, and Next.js's
118
+ * non-standard `next` (`{ revalidate, tags }`). `method`, `headers`, `body`, `signal`, and
119
+ * `credentials` are always controlled by the runtime and override anything set here.
120
+ */
121
+ export type FetchOptions = RequestInit & { next?: Record<string, unknown> }
122
+
114
123
  /**
115
124
  * The request a generated function hands to the runtime. `body` / `headers` / `path` / `query` come
116
125
  * from the grouped options; everything else is plain request configuration.
@@ -126,6 +135,7 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
126
135
  headers?: HeadersInit
127
136
  signal?: AbortSignal
128
137
  credentials?: RequestCredentials
138
+ options?: FetchOptions
129
139
  contentType?: string
130
140
  responseType?: ResponseType
131
141
  throwOnError?: boolean
@@ -133,7 +143,7 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
133
143
  transport?: Transport<TRequest, TResponse>
134
144
  querySerializer?: QuerySerializer
135
145
  bodySerializer?: BodySerializer
136
- parser?: { request?: Parser; response?: Parser }
146
+ parser?: { request?: Parser; response?: Parser; error?: Parser }
137
147
  security?: Array<Auth>
138
148
  auth?: AuthResolver
139
149
  }
@@ -159,6 +169,7 @@ export type ClientConfig<TRequest = Request, TResponse = Response> = {
159
169
  baseURL?: string
160
170
  headers?: HeadersInit
161
171
  credentials?: RequestCredentials
172
+ options?: FetchOptions
162
173
  throwOnError?: boolean
163
174
  transport?: Transport<TRequest, TResponse>
164
175
  querySerializer?: QuerySerializer
@@ -177,6 +188,7 @@ export type ResolvedRequest = {
177
188
  body?: BodyInit
178
189
  signal?: AbortSignal
179
190
  credentials?: RequestCredentials
191
+ options?: FetchOptions
180
192
  responseType?: ResponseType
181
193
  }
182
194
 
@@ -283,13 +295,30 @@ function isFormBody(body: unknown): body is BodyInit {
283
295
  )
284
296
  }
285
297
 
298
+ function appendFormDataValue(formData: FormData, key: string, value: unknown): void {
299
+ if (value === undefined || value === null) return
300
+ if (value instanceof Blob) formData.append(key, value)
301
+ else if (value instanceof Date) formData.append(key, value.toISOString())
302
+ else if (typeof value === 'object') formData.append(key, JSON.stringify(value))
303
+ else formData.append(key, String(value))
304
+ }
305
+
286
306
  /**
287
307
  * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
288
- * For `application/x-www-form-urlencoded` plain objects become `URLSearchParams`.
308
+ * For `multipart/form-data` plain objects become `FormData` and for
309
+ * `application/x-www-form-urlencoded` they become `URLSearchParams`.
289
310
  */
290
311
  export const defaultBodySerializer: BodySerializer = (body, contentType) => {
291
312
  if (body === undefined || body === null) return undefined
292
313
  if (isFormBody(body)) return body as BodyInit
314
+ if (contentType?.includes('multipart/form-data')) {
315
+ const formData = new FormData()
316
+ for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
317
+ if (Array.isArray(value)) for (const item of value) appendFormDataValue(formData, key, item)
318
+ else appendFormDataValue(formData, key, value)
319
+ }
320
+ return formData
321
+ }
293
322
  if (contentType?.includes('application/x-www-form-urlencoded')) {
294
323
  return new URLSearchParams(body as Record<string, string>)
295
324
  }
@@ -439,9 +468,7 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
439
468
  const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
440
469
 
441
470
  const headers = mergeHeaders(config.headers, requestConfig.headers)
442
- if (requestConfig.contentType && requestConfig.contentType !== 'multipart/form-data') {
443
- headers['Content-Type'] = requestConfig.contentType
444
- }
471
+ const requestContentType = requestConfig.contentType ?? headers['Content-Type'] ?? headers['content-type']
445
472
 
446
473
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
447
474
 
@@ -454,15 +481,26 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
454
481
 
455
482
  const rawBody = requestConfig.body
456
483
  const validatedBody = await runParser(requestConfig.parser?.request, rawBody)
484
+ const body = bodySerializer(validatedBody, requestContentType)
485
+ // A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary.
486
+ if (body instanceof FormData) {
487
+ delete headers['Content-Type']
488
+ delete headers['content-type']
489
+ } else if (requestConfig.contentType) {
490
+ headers['Content-Type'] = requestConfig.contentType
491
+ }
457
492
  const url = serializeUrl([config.baseURL, requestConfig.baseURL, requestConfig.url], requestConfig.path ?? {}, querySerializer(query))
458
493
 
494
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
495
+
459
496
  let resolvedRequest: ResolvedRequest = {
460
497
  url,
461
498
  method: (requestConfig.method ?? 'GET').toUpperCase(),
462
499
  headers,
463
- body: bodySerializer(validatedBody, headers['Content-Type'] ?? headers['content-type']),
500
+ body,
464
501
  signal: requestConfig.signal,
465
502
  credentials: requestConfig.credentials,
503
+ options,
466
504
  responseType: requestConfig.responseType,
467
505
  }
468
506
  resolvedRequest = await interceptors.request.run(resolvedRequest)
@@ -486,11 +524,12 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
486
524
  }
487
525
 
488
526
  const data = isSuccess ? await runParser(requestConfig.parser?.response, result.data) : undefined
527
+ const error = isSuccess ? undefined : await runParser(requestConfig.parser?.error, result.data)
489
528
 
490
529
  return {
491
530
  status: result.status,
492
531
  data,
493
- error: isSuccess ? undefined : result.data,
532
+ error,
494
533
  request: result.request,
495
534
  response: result.response,
496
535
  }
@@ -565,6 +604,7 @@ async function parseResponse(response: Response, responseType?: ResponseType): P
565
604
  */
566
605
  const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
567
606
  const init: RequestInit = {
607
+ ...request.options, // cache, mode, redirect, keepalive, duplex, next, …
568
608
  method: request.method,
569
609
  headers: request.headers,
570
610
  body: request.body,