Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-interceptor-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

fix(clients): defer URL construction and thread finalError through interceptors
123 changes: 46 additions & 77 deletions packages/openapi-ts/src/plugins/@hey-api/client-next/bundle/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ export const createClient = (config: Config = {}): Client => {
};

if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
await setAuthParams({ ...opts, security: opts.security });
}

if (opts.requestValidator) {
Expand All @@ -59,34 +56,30 @@ export const createClient = (config: Config = {}): Client => {
opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
}

// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}

const resolvedOpts = opts as typeof opts & ResolvedRequestOptions<ThrowOnError, Url>;
const url = buildUrl(resolvedOpts);

return { opts: resolvedOpts, url };
return {
opts: opts as typeof opts & ResolvedRequestOptions<ThrowOnError, Url>,
};
};

// @ts-expect-error
const request: Client['request'] = async (options) => {
const throwOnError = options.throwOnError ?? _config.throwOnError;

let response: Response | undefined;

try {
const { opts, url } = await beforeRequest(options);
const { opts } = await beforeRequest(options);

// request interceptors
for (const fn of interceptors.request.fns) {
if (fn) {
await fn(opts);
}
if (fn) await fn(opts);
}

// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const url = buildUrl(opts);

const _fetch = opts.fetch!;
const requestInit: ReqInit = {
...opts,
Expand All @@ -95,15 +88,12 @@ export const createClient = (config: Config = {}): Client => {

response = await _fetch(url, requestInit);

// response interceptors
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, opts);
}
if (fn) response = await fn(response, opts);
}

const result = {
response,
};
const result = { response };

if (response.ok) {
const parseAs =
Expand All @@ -112,7 +102,8 @@ export const createClient = (config: Config = {}): Client => {
: opts.parseAs) ?? 'json';

if (response.status === 204 || response.headers.get('Content-Length') === '0') {
let emptyData: any;
let emptyData: any = {};

switch (parseAs) {
case 'arrayBuffer':
case 'blob':
Expand All @@ -125,53 +116,37 @@ export const createClient = (config: Config = {}): Client => {
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
}
return {
data: emptyData,
...result,
};

return { 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 {
data: response.body,
...result,
};
return { data: response.body, ...result };
}

if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}

if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
if (opts.responseValidator) await opts.responseValidator(data);
if (opts.responseTransformer) data = await opts.responseTransformer(data);
}

return {
data,
...result,
};
return { data, ...result };
}

const textError = await response.text();
Expand All @@ -180,7 +155,7 @@ export const createClient = (config: Config = {}): Client => {
try {
jsonError = JSON.parse(textError);
} catch {
// noop
// ignore JSON parse error
}

throw jsonError ?? textError;
Expand All @@ -193,41 +168,35 @@ export const createClient = (config: Config = {}): Client => {
}
}

finalError = finalError || {};
if (throwOnError) throw finalError;

if (throwOnError) {
throw finalError;
}

return {
error: finalError,
response,
};
return { error: finalError || {}, 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);
const { opts } = await beforeRequest(options);

return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
const requestInit = { ...init, url };

onRequest: async (_unusedUrl, init) => {
const clonedOpts = { ...opts };

for (const fn of interceptors.request.fns) {
if (fn) {
await fn(requestInit as ResolvedRequestOptions);
request = new Request(requestInit.url, requestInit);
}
if (fn) await fn(clonedOpts);
}
return request;

const finalizedUrl = buildUrl(clonedOpts);
return new Request(finalizedUrl, init);
},

serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
url,
});
};

Expand All @@ -249,15 +218,15 @@ export const createClient = (config: Config = {}): Client => {
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'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
head: makeMethodFn('HEAD'),
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
trace: makeMethodFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
} as unknown as Client;
};
Loading