UNPKG

@hey-api/openapi-ts

Version:

🌀 OpenAPI to TypeScript code generator. Generate API clients, SDKs, validators, and more.

286 lines (249 loc) • 8.83 kB
import { ofetch, type ResponseType as OfetchResponseType } from 'ofetch'; import { createSseClient } from '../core/serverSentEvents'; import type { HttpMethod } from '../core/types'; import { getValidRequestBody } from '../core/utils'; import type { Client, Config, RequestOptions, ResolvedRequestOptions, } from './types'; import { buildOfetchOptions, buildUrl, createConfig, createInterceptors, isRepeatableBody, mapParseAsToResponseType, mergeConfigs, mergeHeaders, parseError, parseSuccess, setAuthParams, wrapDataReturn, wrapErrorReturn, } from './utils'; type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { body?: BodyInit | null | undefined; headers: ReturnType<typeof mergeHeaders>; }; export const createClient = (config: Config = {}): Client => { let _config = mergeConfigs(createConfig(), config); const getConfig = (): Config => ({ ..._config }); const setConfig = (config: Config): Config => { _config = mergeConfigs(_config, config); return getConfig(); }; const interceptors = createInterceptors< Request, Response, unknown, ResolvedRequestOptions >(); // precompute serialized / network body const resolveOptions = async (options: RequestOptions) => { const opts = { ..._config, ...options, headers: mergeHeaders(_config.headers, options.headers), serializedBody: undefined, }; if (opts.security) { await setAuthParams({ ...opts, security: opts.security, }); } if (opts.requestValidator) { await opts.requestValidator(opts); } if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type if body is empty to avoid invalid requests if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } // if a raw body is provided (no serializer), adjust Content-Type only when it // equals the default JSON value to better match the concrete body type if ( opts.body !== undefined && opts.bodySerializer === null && (opts.headers.get('Content-Type') || '').toLowerCase() === 'application/json' ) { const b: unknown = opts.body; if (typeof FormData !== 'undefined' && b instanceof FormData) { // let the runtime set the multipart boundary opts.headers.delete('Content-Type'); } else if ( typeof URLSearchParams !== 'undefined' && b instanceof URLSearchParams ) { // standard urlencoded content type (+ charset) opts.headers.set( 'Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8', ); } else if (typeof Blob !== 'undefined' && b instanceof Blob) { const t = b.type?.trim(); if (t) { opts.headers.set('Content-Type', t); } else { // unknown blob type: avoid sending a misleading JSON header opts.headers.delete('Content-Type'); } } } // precompute network body (stability for retries and interceptors) const networkBody = getValidRequestBody(opts) as | RequestInit['body'] | null | undefined; const url = buildUrl(opts); return { networkBody, opts, url }; }; // apply request interceptors and mirror header/method/signal back to opts const applyRequestInterceptors = async ( request: Request, opts: ResolvedRequestOptions, body: BodyInit | null | undefined, ) => { for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } } // reflect interceptor changes into opts used by the network layer opts.headers = request.headers; opts.method = request.method as Uppercase<HttpMethod>; // ignore request.body changes to avoid turning serialized bodies into streams // body comes only from getValidRequestBody(options) // reflect signal if present opts.signal = (request as any).signal as AbortSignal | undefined; // When body is FormData, remove Content-Type header to avoid boundary mismatch. // Note: We already delete Content-Type in resolveOptions for FormData, but the // Request constructor (line 175) re-adds it with an auto-generated boundary. // Since we pass the original FormData (not the Request's body) to ofetch, and // ofetch will generate its own boundary, we must remove the Request's Content-Type // to let ofetch set the correct one. Otherwise the boundary in the header won't // match the boundary in the actual multipart body sent by ofetch. if (typeof FormData !== 'undefined' && body instanceof FormData) { opts.headers.delete('Content-Type'); } return request; }; // build ofetch options with stable retry logic based on body repeatability const buildNetworkOptions = ( opts: ResolvedRequestOptions, body: BodyInit | null | undefined, responseType: OfetchResponseType | undefined, ) => { const effectiveRetry = isRepeatableBody(body) ? (opts.retry as any) : (0 as any); return buildOfetchOptions(opts, body, responseType, effectiveRetry); }; const request: Client['request'] = async (options) => { const { networkBody: initialNetworkBody, opts, url, } = await resolveOptions(options as any); // map parseAs -> ofetch responseType once per request const ofetchResponseType: OfetchResponseType | undefined = mapParseAsToResponseType(opts.parseAs, opts.responseType); const $ofetch = opts.ofetch ?? ofetch; // create Request before network to run middleware consistently const networkBody = initialNetworkBody; const requestInit: ReqInit = { body: networkBody, headers: opts.headers as Headers, method: opts.method, redirect: 'follow', signal: opts.signal, }; let request = new Request(url, requestInit); request = await applyRequestInterceptors(request, opts, networkBody); const finalUrl = request.url; // build ofetch options and perform the request (.raw keeps the Response) const responseOptions = buildNetworkOptions( opts as ResolvedRequestOptions, networkBody, ofetchResponseType, ); let response = await $ofetch.raw(finalUrl, responseOptions); for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } } const result = { request, response }; if (response.ok) { const data = await parseSuccess(response, opts, ofetchResponseType); return wrapDataReturn(data, result, opts.responseStyle); } let finalError = await parseError(response); for (const fn of interceptors.error.fns) { if (fn) { finalError = await fn(finalError, response, request, opts); } } // ensure error is never undefined after interceptors finalError = (finalError as any) || ({} as string); if (opts.throwOnError) { throw finalError; } return wrapErrorReturn(finalError, result, opts.responseStyle) as any; }; const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => request({ ...options, method } as any); const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { const { networkBody, opts, url } = await resolveOptions(options); const optsForSse: any = { ...opts }; delete optsForSse.body; // body is provided via serializedBody below return createSseClient({ ...optsForSse, fetch: opts.fetch, headers: opts.headers as Headers, method, onRequest: async (url, init) => { let request = new Request(url, init); request = await applyRequestInterceptors(request, opts, networkBody); return request; }, serializedBody: networkBody as BodyInit | null | undefined, signal: opts.signal, url, }); }; return { buildUrl, connect: makeMethodFn('CONNECT'), delete: makeMethodFn('DELETE'), get: makeMethodFn('GET'), getConfig, head: makeMethodFn('HEAD'), interceptors, options: makeMethodFn('OPTIONS'), patch: makeMethodFn('PATCH'), post: makeMethodFn('POST'), put: makeMethodFn('PUT'), request, setConfig, sse: { connect: makeSseFn('CONNECT'), delete: makeSseFn('DELETE'), get: makeSseFn('GET'), head: makeSseFn('HEAD'), options: makeSseFn('OPTIONS'), patch: makeSseFn('PATCH'), post: makeSseFn('POST'), put: makeSseFn('PUT'), trace: makeSseFn('TRACE'), }, trace: makeMethodFn('TRACE'), } as Client; };