refactor(ui): move request base resolution into the shared resolveApiBase module

api.ts owned the base-vs-origin precedence, the trailing-slash trim and the
base+path join inline. That logic belongs with the rest of base resolution and
was only reachable through a fetch client, so it could not be tested directly.

Extract resolveRequestUrl into resolveApiBase.ts with its own unit tests.
api.ts now only wires the shared resolver into openapi-fetch's Request option.
No behaviour change: same precedence, same trimming, same output.
This commit is contained in:
Yuneng Jiang 2026-08-04 13:13:04 -07:00
parent 930451fe31
commit d158cf187b
No known key found for this signature in database
3 changed files with 55 additions and 4 deletions

View file

@ -3,11 +3,14 @@ import createQueryClient from "openapi-react-query";
import type { paths } from "./schema";
import { ApiError, deriveErrorMessage } from "./client";
import { getAuthHeaderName, getAuthToken, getRequestBaseUrl, reportError } from "./runtime";
const resolveRequestBase = (): string => (getRequestBaseUrl() || globalThis.location?.origin || "").replace(/\/+$/, "");
import { resolveRequestUrl } from "./resolveApiBase";
const BaseAwareRequest = function (url: string, init?: RequestInit): Request {
return new globalThis.Request(`${resolveRequestBase()}${url}`, init);
const target = resolveRequestUrl(url, {
registeredBase: getRequestBaseUrl(),
pageOrigin: globalThis.location?.origin,
});
return new globalThis.Request(target, init);
} as unknown as typeof Request;
const middleware: Middleware = {

View file

@ -1,5 +1,41 @@
import { describe, expect, it } from "vitest";
import { resolveApiBase } from "./resolveApiBase";
import { resolveApiBase, resolveRequestUrl } from "./resolveApiBase";
describe("resolveRequestUrl", () => {
it("targets the registered base when one is registered", () => {
expect(
resolveRequestUrl("/model_group/info", {
registeredBase: "https://proxy.example.com",
pageOrigin: "http://localhost:3000",
}),
).toBe("https://proxy.example.com/model_group/info");
});
it("falls back to the page origin when no base is registered", () => {
expect(resolveRequestUrl("/model_group/info", { registeredBase: "", pageOrigin: "http://localhost:3000" })).toBe(
"http://localhost:3000/model_group/info",
);
});
it("trims a trailing slash so the path is not doubled up", () => {
expect(resolveRequestUrl("/model_group/info", { registeredBase: "https://proxy.example.com/" })).toBe(
"https://proxy.example.com/model_group/info",
);
});
it("keeps the path relative when neither a base nor an origin is available", () => {
expect(resolveRequestUrl("/model_group/info", {})).toBe("/model_group/info");
expect(resolveRequestUrl("/model_group/info", { registeredBase: null, pageOrigin: null })).toBe(
"/model_group/info",
);
});
it("preserves an already-serialized query string", () => {
expect(
resolveRequestUrl("/model_group/info?model_group=gpt-4o", { registeredBase: "https://proxy.example.com" }),
).toBe("https://proxy.example.com/model_group/info?model_group=gpt-4o");
});
});
describe("resolveApiBase", () => {
describe("same-origin (no explicit base)", () => {

View file

@ -33,3 +33,15 @@ export const resolveApiBase = ({ explicitBase, serverRootPath }: ApiBaseInputs):
if (rootPath === "" || base.endsWith(rootPath)) return base;
return `${base}${rootPath}`;
};
export interface RequestUrlInputs {
/** Base registered at runtime (a split-origin proxy or worker URL); empty means none. */
registeredBase?: string | null;
/** Origin of the page issuing the request; the same-origin fallback. */
pageOrigin?: string | null;
}
export const resolveRequestUrl = (path: string, { registeredBase, pageOrigin }: RequestUrlInputs): string => {
const base = (registeredBase || pageOrigin || "").replace(/\/+$/, "");
return `${base}${path}`;
};