UNPKG

@hey-api/openapi-ts

Version:

🌀 OpenAPI to TypeScript codegen. Production-grade SDKs, Zod schemas, TanStack Query hooks, and 20+ plugins. Used by Vercel, OpenCode, and PayPal.

361 lines (311 loc) • 10.8 kB
import type { KyResponse, Options as KyOptions } from 'ky'; import ky, { isHTTPError } from 'ky'; 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 type { Middleware } from './utils'; import { buildUrl, createConfig, createInterceptors, getParseAs, mergeConfigs, mergeHeaders, setAuthParams, } from './utils'; 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>(); const beforeRequest = async < TData = unknown, TResponseStyle extends 'data' | 'fields' = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, >( options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>, ) => { const opts = { ..._config, ...options, headers: mergeHeaders(_config.headers, options.headers), ky: options.ky ?? _config.ky ?? ky, // deep merge kyOptions to ensure base _config is being respected kyOptions: { ..._config.kyOptions, ...options.kyOptions, }, serializedBody: undefined as string | undefined, }; if (opts.security) { await setAuthParams(opts); } if (opts.requestValidator) { await opts.requestValidator(opts); } if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; } if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>; const url = buildUrl(resolvedOpts); return { opts: resolvedOpts, url }; }; const parseErrorResponse = async ( response: Response, request: Request, opts: ResolvedRequestOptions, interceptorsMiddleware: Middleware<Request, Response, unknown, ResolvedRequestOptions>, ) => { const result = { request, response, }; const textError = await response.text(); let jsonError: unknown; try { jsonError = JSON.parse(textError); } catch { jsonError = undefined; } const error = jsonError ?? textError; let finalError = error; for (const fn of interceptorsMiddleware.error.fns) { if (fn) { finalError = (await fn(finalError, response, request, opts)) as string; } } finalError = finalError || ({} as string); if (opts.throwOnError) { throw finalError; } return opts.responseStyle === 'data' ? undefined : { error: finalError, ...result, }; }; const request: Client['request'] = async (options) => { const throwOnError = options.throwOnError ?? _config.throwOnError; const responseStyle = options.responseStyle ?? _config.responseStyle; let request: Request | undefined; let response: KyResponse | undefined; let errorInterceptorsInvoked = false; try { const { opts, url } = await beforeRequest(options); const kyInstance = opts.ky!; const validBody = getValidRequestBody(opts); const kyOptions: KyOptions = { body: validBody as BodyInit, ...(opts.cache !== undefined ? { cache: opts.cache } : {}), ...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}), ...(opts.headers !== undefined ? { headers: opts.headers } : {}), ...(opts.integrity !== undefined ? { integrity: opts.integrity } : {}), ...(opts.keepalive !== undefined ? { keepalive: opts.keepalive } : {}), ...(opts.method !== undefined ? { method: opts.method } : {}), ...(opts.mode !== undefined ? { mode: opts.mode } : {}), redirect: opts.redirect ?? 'follow', ...(opts.referrer !== undefined ? { referrer: opts.referrer } : {}), ...(opts.referrerPolicy !== undefined ? { referrerPolicy: opts.referrerPolicy } : {}), ...(opts.signal !== undefined ? { signal: opts.signal } : {}), throwHttpErrors: opts.throwOnError ?? false, ...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}), ...opts.kyOptions, retry: opts.retry ?? opts.kyOptions?.retry ?? 2, }; request = new Request(url, { body: kyOptions.body, headers: kyOptions.headers as HeadersInit, method: kyOptions.method, }); for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } } try { response = await kyInstance(request, kyOptions); } catch (error) { if (isHTTPError(error)) { response = error.response; for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } } // parseErrorResponse will run error interceptors, and re-throw when // throwOnError is true, which bubbles already intercepted error to // outer catch. With this flag, we can avoid outer catch running interceptors again errorInterceptorsInvoked = true; return parseErrorResponse(response, request, opts, interceptors); } throw error; } for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } } const result = { request, response, }; if (response.ok) { const parseAs = (opts.parseAs === 'auto' ? getParseAs(response.headers.get('Content-Type')) : opts.parseAs) ?? 'json'; if (response.status === 204 || response.headers.get('Content-Length') === '0') { let emptyData: any; switch (parseAs) { case 'arrayBuffer': case 'blob': case 'text': emptyData = await response[parseAs](); break; case 'formData': emptyData = new FormData(); break; case 'stream': emptyData = response.body; break; case 'json': default: emptyData = {}; break; } return opts.responseStyle === 'data' ? emptyData : { data: emptyData, ...result, }; } let data: any; switch (parseAs) { case 'arrayBuffer': case 'blob': case 'formData': case 'text': data = await response[parseAs](); break; case 'json': { // Some servers return 200 with no Content-Length and empty body. // response.json() would throw; read as text and parse if non-empty. const text = await response.text(); data = text ? JSON.parse(text) : {}; break; } case 'stream': return opts.responseStyle === 'data' ? response.body : { data: response.body, ...result, }; } if (parseAs === 'json') { if (opts.responseValidator) { await opts.responseValidator(data); } if (opts.responseTransformer) { data = await opts.responseTransformer(data); } } return opts.responseStyle === 'data' ? data : { data, ...result, }; } // parseErrorResponse will run error interceptors, and re-throw when // throwOnError is true, which bubbles already intercepted error to // outer catch. With this flag, we can avoid outer catch running interceptors again errorInterceptorsInvoked = true; return parseErrorResponse(response, request, opts, interceptors); } catch (error) { let finalError = error; // error may already be processed by parseErrorResponse, in this case // we can skip running interceptors again if (!errorInterceptorsInvoked) { // run error interceptors for errors not already handled by parseErrorResponse for (const fn of interceptors.error.fns) { if (fn) { finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); } } finalError = finalError || ({} as string); } if (throwOnError) { throw finalError; } return responseStyle === 'data' ? undefined : { error: finalError, request, response, }; } }; const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => request({ ...options, method }); const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { const { opts, url } = await beforeRequest(options); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, fetch: globalThis.fetch, method, onRequest: async (url, init) => { let request = new Request(url, init); for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } } return request; }, serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, url, }); }; const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); return { buildUrl: _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; };