diff --git a/.changeset/fix-interceptor-order.md b/.changeset/fix-interceptor-order.md new file mode 100644 index 0000000000..c62363e334 --- /dev/null +++ b/.changeset/fix-interceptor-order.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +fix(clients): defer URL construction and thread finalError through interceptors \ No newline at end of file diff --git a/.gitignore b/.gitignore index c4a23f3d0c..ddb13170a0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ openapi-ts-error-* # But DO NOT ignore generated snapshots! !test/__snapshots__/test/generated !test/__snapshots__/test/generated/** + +# gstack (global install) +.amazonq/rules/gstack.md diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-next/bundle/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-next/bundle/client.ts index a8e6070335..dd2e602b31 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-next/bundle/client.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-next/bundle/client.ts @@ -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) { @@ -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; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; + return { + opts: opts as typeof opts & ResolvedRequestOptions, + }; }; - // @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, @@ -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 = @@ -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': @@ -125,18 +116,13 @@ 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': @@ -144,34 +130,23 @@ export const createClient = (config: Config = {}): Client => { 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(); @@ -180,7 +155,7 @@ export const createClient = (config: Config = {}): Client => { try { jsonError = JSON.parse(textError); } catch { - // noop + // ignore JSON parse error } throw jsonError ?? textError; @@ -193,16 +168,9 @@ export const createClient = (config: Config = {}): Client => { } } - finalError = finalError || {}; + if (throwOnError) throw finalError; - if (throwOnError) { - throw finalError; - } - - return { - error: finalError, - response, - }; + return { error: finalError || {}, response }; } }; @@ -210,24 +178,25 @@ export const createClient = (config: Config = {}): Client => { request({ ...options, method }); const makeSseFn = (method: Uppercase) => 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, }); }; @@ -236,6 +205,7 @@ export const createClient = (config: Config = {}): Client => { return { buildUrl: _buildUrl, connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), get: makeMethodFn('GET'), getConfig, @@ -249,15 +219,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; };