From 29a6629ee9aeb158a9f7a8d62f55815ae3b471c5 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 29 Apr 2026 01:32:28 -0700 Subject: [PATCH 1/2] fix(client-next): build URL after request interceptors --- .../fix-client-next-interceptor-order.md | 7 ++ .../base-url-false/client/client.gen.ts | 15 +++- .../base-url-number/client/client.gen.ts | 15 +++- .../base-url-strict/client/client.gen.ts | 15 +++- .../base-url-string/client/client.gen.ts | 15 +++- .../clean-false/client/client.gen.ts | 15 +++- .../client-next/default/client/client.gen.ts | 15 +++- .../client/client.gen.ts | 15 +++- .../sdk-client-optional/client/client.gen.ts | 15 +++- .../sdk-client-required/client/client.gen.ts | 15 +++- .../tsconfig-node16-sdk/client/client.gen.ts | 15 +++- .../client/client.gen.ts | 15 +++- .../3.1.x/sse-next/client/client.gen.ts | 15 +++- .../client-next/__tests__/client.test.ts | 83 +++++++++++++++++++ .../@hey-api/client-next/bundle/client.ts | 15 +++- 15 files changed, 233 insertions(+), 52 deletions(-) create mode 100644 .changeset/fix-client-next-interceptor-order.md diff --git a/.changeset/fix-client-next-interceptor-order.md b/.changeset/fix-client-next-interceptor-order.md new file mode 100644 index 0000000000..fe3d4cb287 --- /dev/null +++ b/.changeset/fix-client-next-interceptor-order.md @@ -0,0 +1,7 @@ +--- +"@hey-api/openapi-ts": patch +--- + +Fix `@hey-api/client-next` request interceptors so URL mutations are reflected in the final request URL. + +Previously, the client built the URL before request interceptors ran, so interceptor changes to `baseUrl`, `url`, `path`, or `query` were ignored by the fetch call. diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-false/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-false/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-false/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-false/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-number/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-number/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-number/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-number/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-strict/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-strict/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-strict/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-strict/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-string/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-string/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-string/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/base-url-string/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/clean-false/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/clean-false/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/clean-false/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/clean-false/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/default/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/default/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/default/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/default/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/import-file-extension-ts/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/import-file-extension-ts/client/client.gen.ts index 465230e975..0435323598 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/import-file-extension-ts/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/import-file-extension-ts/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-optional/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-optional/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-optional/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-optional/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-required/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-required/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-required/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/sdk-client-required/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-node16-sdk/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-node16-sdk/client/client.gen.ts index fe58f5be3a..a13cb429e9 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-node16-sdk/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-node16-sdk/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-nodenext-sdk/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-nodenext-sdk/client/client.gen.ts index fe58f5be3a..a13cb429e9 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-nodenext-sdk/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-next/tsconfig-nodenext-sdk/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-next/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-next/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-next/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-next/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-next/__tests__/client.test.ts b/packages/openapi-ts/src/plugins/@hey-api/client-next/__tests__/client.test.ts index e07e1bf69f..af64a19f63 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-next/__tests__/client.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-next/__tests__/client.test.ts @@ -286,3 +286,86 @@ describe('request interceptor', () => { }, ); }); + +describe('request interceptor URL mutation', () => { + // Regression tests for a bug where the final URL was computed before request + // interceptors ran, causing interceptor mutations to `opts.baseUrl`, + // `opts.url`, `opts.path`, and `opts.query` to be ignored. + const buildOkResponse = () => + new Response(JSON.stringify({ success: true }), { + headers: { 'Content-Type': 'application/json' }, + status: 200, + }); + + it('honors interceptor mutations to opts.baseUrl in the fetched URL', async () => { + const client = createClient({ baseUrl: 'https://example.com' }); + const mockFetch: MockFetch = vi.fn().mockResolvedValue(buildOkResponse()); + + const interceptorId = client.interceptors.request.use((options: ResolvedRequestOptions) => { + options.baseUrl = 'https://rerouted.com'; + }); + + await client.get({ fetch: mockFetch, url: '/test' }); + + expect(mockFetch).toHaveBeenCalledExactlyOnceWith( + 'https://rerouted.com/test', + expect.any(Object), + ); + + client.interceptors.request.eject(interceptorId); + }); + + it('honors interceptor mutations to opts.url in the fetched URL', async () => { + const client = createClient({ baseUrl: 'https://example.com' }); + const mockFetch: MockFetch = vi.fn().mockResolvedValue(buildOkResponse()); + + const interceptorId = client.interceptors.request.use((options: ResolvedRequestOptions) => { + options.url = '/rewritten'; + }); + + await client.get({ fetch: mockFetch, url: '/original' }); + + expect(mockFetch).toHaveBeenCalledExactlyOnceWith( + 'https://example.com/rewritten', + expect.any(Object), + ); + + client.interceptors.request.eject(interceptorId); + }); + + it('honors interceptor mutations to opts.path in the fetched URL', async () => { + const client = createClient({ baseUrl: 'https://example.com' }); + const mockFetch: MockFetch = vi.fn().mockResolvedValue(buildOkResponse()); + + const interceptorId = client.interceptors.request.use((options: ResolvedRequestOptions) => { + options.path = { id: 42 }; + }); + + await client.get({ fetch: mockFetch, url: '/items/{id}' }); + + expect(mockFetch).toHaveBeenCalledExactlyOnceWith( + 'https://example.com/items/42', + expect.any(Object), + ); + + client.interceptors.request.eject(interceptorId); + }); + + it('honors interceptor mutations to opts.query in the fetched URL', async () => { + const client = createClient({ baseUrl: 'https://example.com' }); + const mockFetch: MockFetch = vi.fn().mockResolvedValue(buildOkResponse()); + + const interceptorId = client.interceptors.request.use((options: ResolvedRequestOptions) => { + options.query = { bar: 'baz' }; + }); + + await client.get({ fetch: mockFetch, url: '/items' }); + + expect(mockFetch).toHaveBeenCalledExactlyOnceWith( + 'https://example.com/items?bar=baz', + expect.any(Object), + ); + + client.interceptors.request.eject(interceptorId); + }); +}); 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..c4b53535ba 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 @@ -65,9 +65,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -77,7 +76,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -85,6 +84,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -210,7 +213,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, From 619f6752ea129d2d79355591fc8a5263a9c8a525 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 29 Apr 2026 01:36:08 -0700 Subject: [PATCH 2/2] chore: update next example client --- .../src/client/client/client.gen.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/openapi-ts-next/src/client/client/client.gen.ts b/examples/openapi-ts-next/src/client/client/client.gen.ts index 1e8882f023..2f471528dd 100644 --- a/examples/openapi-ts-next/src/client/client/client.gen.ts +++ b/examples/openapi-ts-next/src/client/client/client.gen.ts @@ -67,9 +67,8 @@ export const createClient = (config: Config = {}): Client => { } const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - return { opts: resolvedOpts, url }; + return { opts: resolvedOpts }; }; // @ts-expect-error @@ -79,7 +78,7 @@ export const createClient = (config: Config = {}): Client => { let response: Response | undefined; try { - const { opts, url } = await beforeRequest(options); + const { opts } = await beforeRequest(options); for (const fn of interceptors.request.fns) { if (fn) { @@ -87,6 +86,10 @@ export const createClient = (config: Config = {}): Client => { } } + // Build the URL after request interceptors have run so any mutations to + // `opts.baseUrl`, `opts.url`, `opts.path`, or `opts.query` are honored. + const url = buildUrl(opts); + // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; @@ -212,7 +215,11 @@ 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); + // The SSE path applies request interceptors again inside `onRequest` + // (see below), so the URL we seed `createSseClient` with is only the + // initial value; any per-request URL mutation happens there. + const url = buildUrl(opts); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined,