@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
text/typescript
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;
};