@kubb/plugin-fetch 5.0.0-beta.73 → 5.0.0-beta.76

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
 
@@ -133,7 +134,7 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
133
134
  transport?: Transport<TRequest, TResponse>
134
135
  querySerializer?: QuerySerializer
135
136
  bodySerializer?: BodySerializer
136
- parser?: { request?: Parser; response?: Parser }
137
+ parser?: { request?: Parser; response?: Parser; error?: Parser }
137
138
  security?: Array<Auth>
138
139
  auth?: AuthResolver
139
140
  }
@@ -243,6 +244,7 @@ export type ClientInstance<TRequest = Request, TResponse = Response> = {
243
244
  <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
244
245
  getConfig: () => ClientConfig<TRequest, TResponse>
245
246
  setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>
247
+ getUrl: <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>) => string
246
248
  interceptors: Interceptors<TRequest, TResponse>
247
249
  createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
248
250
  }
@@ -282,13 +284,30 @@ function isFormBody(body: unknown): body is BodyInit {
282
284
  )
283
285
  }
284
286
 
287
+ function appendFormDataValue(formData: FormData, key: string, value: unknown): void {
288
+ if (value === undefined || value === null) return
289
+ if (value instanceof Blob) formData.append(key, value)
290
+ else if (value instanceof Date) formData.append(key, value.toISOString())
291
+ else if (typeof value === 'object') formData.append(key, JSON.stringify(value))
292
+ else formData.append(key, String(value))
293
+ }
294
+
285
295
  /**
286
296
  * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
287
- * For `application/x-www-form-urlencoded` plain objects become `URLSearchParams`.
297
+ * For `multipart/form-data` plain objects become `FormData` and for
298
+ * `application/x-www-form-urlencoded` they become `URLSearchParams`.
288
299
  */
289
300
  export const defaultBodySerializer: BodySerializer = (body, contentType) => {
290
301
  if (body === undefined || body === null) return undefined
291
302
  if (isFormBody(body)) return body as BodyInit
303
+ if (contentType?.includes('multipart/form-data')) {
304
+ const formData = new FormData()
305
+ for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
306
+ if (Array.isArray(value)) for (const item of value) appendFormDataValue(formData, key, item)
307
+ else appendFormDataValue(formData, key, value)
308
+ }
309
+ return formData
310
+ }
292
311
  if (contentType?.includes('application/x-www-form-urlencoded')) {
293
312
  return new URLSearchParams(body as Record<string, string>)
294
313
  }
@@ -337,6 +356,19 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
337
356
  return Object.assign({}, ...sources.map(serializeHeaders))
338
357
  }
339
358
 
359
+ /**
360
+ * Joins the base and request URL parts, interpolates `{param}` segments from the path params
361
+ * (URL-encoded), and appends the serialized query. Shared by the send path and `getUrl` so both
362
+ * produce an identical URL.
363
+ */
364
+ function serializeUrl(parts: Array<string | undefined>, pathParams: Record<string, unknown>, search: string): string {
365
+ const path = parts
366
+ .filter(Boolean)
367
+ .join('')
368
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
369
+ return path + (search ? `?${search}` : '')
370
+ }
371
+
340
372
  /**
341
373
  * Creates a transport-agnostic interceptor channel. Interceptors run in registration order; `eject`
342
374
  * removes one by id and `update` swaps its function in place without reordering.
@@ -425,9 +457,7 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
425
457
  const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
426
458
 
427
459
  const headers = mergeHeaders(config.headers, requestConfig.headers)
428
- if (requestConfig.contentType && requestConfig.contentType !== 'multipart/form-data') {
429
- headers['Content-Type'] = requestConfig.contentType
430
- }
460
+ const requestContentType = requestConfig.contentType ?? headers['Content-Type'] ?? headers['content-type']
431
461
 
432
462
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
433
463
 
@@ -440,19 +470,21 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
440
470
 
441
471
  const rawBody = requestConfig.body
442
472
  const validatedBody = await runParser(requestConfig.parser?.request, rawBody)
443
- const search = querySerializer(query)
444
- const pathParams = requestConfig.path ?? {}
445
- const interpolatedUrl = [config.baseURL, requestConfig.baseURL, requestConfig.url]
446
- .filter(Boolean)
447
- .join('')
448
- .replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
449
- const url = interpolatedUrl + (search ? `?${search}` : '')
473
+ const body = bodySerializer(validatedBody, requestContentType)
474
+ // A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary.
475
+ if (body instanceof FormData) {
476
+ delete headers['Content-Type']
477
+ delete headers['content-type']
478
+ } else if (requestConfig.contentType) {
479
+ headers['Content-Type'] = requestConfig.contentType
480
+ }
481
+ const url = serializeUrl([config.baseURL, requestConfig.baseURL, requestConfig.url], requestConfig.path ?? {}, querySerializer(query))
450
482
 
451
483
  let resolvedRequest: ResolvedRequest = {
452
484
  url,
453
485
  method: (requestConfig.method ?? 'GET').toUpperCase(),
454
486
  headers,
455
- body: bodySerializer(validatedBody, headers['Content-Type'] ?? headers['content-type']),
487
+ body,
456
488
  signal: requestConfig.signal,
457
489
  credentials: requestConfig.credentials,
458
490
  responseType: requestConfig.responseType,
@@ -478,11 +510,12 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
478
510
  }
479
511
 
480
512
  const data = isSuccess ? await runParser(requestConfig.parser?.response, result.data) : undefined
513
+ const error = isSuccess ? undefined : await runParser(requestConfig.parser?.error, result.data)
481
514
 
482
515
  return {
483
516
  status: result.status,
484
517
  data,
485
- error: isSuccess ? undefined : result.data,
518
+ error,
486
519
  request: result.request,
487
520
  response: result.response,
488
521
  }
@@ -493,6 +526,11 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
493
526
  config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
494
527
  return config
495
528
  }
529
+ client.getUrl = (requestConfig) => {
530
+ const querySerializer = requestConfig.querySerializer ?? config.querySerializer ?? defaultQuerySerializer
531
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
532
+ return serializeUrl([config.baseURL, requestConfig.baseURL, requestConfig.url], requestConfig.path ?? {}, querySerializer(query))
533
+ }
496
534
  client.interceptors = interceptors
497
535
  client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next })
498
536