mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
* fix(client): rewrite absolute fetch/EventSource paths under basePath
Absolute browser calls like fetch("/api/...") and new EventSource("/api/...")
do not honor Next.js basePath, so subpath deploys (OMNIROUTE_BASE_PATH) break
dashboard health checks, settings APIs, and SSE unless a reverse proxy rewrites
the domain root.
- Add withBasePath / getDeployBasePath helpers
- Install ref-counted fetch + EventSource rewrite when basePath is set
(same pattern as installDashboardCsrfFetch)
- Mount BasePathNetworkProvider at the root so login works too
- Mirror OMNIROUTE_BASE_PATH to NEXT_PUBLIC_OMNIROUTE_BASE_PATH for the client
- Document in .env.example; unit tests for rewrite rules
* docs(changelog): add fragment for #8515 basePath client fetch
* test(client): move basePath tests into a scanned dir and fix no-op call-shape asserts
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(client-sweep-8515): restore CHANGELOG #8471 bullet and fix basePath TS2322
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
/**
|
|
* Install a same-origin fetch + EventSource rewrite for Next.js basePath deploys.
|
|
*
|
|
* Pattern mirrors `installDashboardCsrfFetch` (ref-counted global wrap).
|
|
* Only activates when `NEXT_PUBLIC_OMNIROUTE_BASE_PATH` / `OMNIROUTE_BASE_PATH` is set.
|
|
*/
|
|
|
|
import { getDeployBasePath, withBasePath } from "./basePath";
|
|
|
|
let originalFetch: typeof fetch | null = null;
|
|
let originalEventSource: typeof EventSource | null = null;
|
|
let installCount = 0;
|
|
|
|
export function __resetBasePathFetchForTests(): void {
|
|
if (originalFetch) {
|
|
globalThis.fetch = originalFetch;
|
|
originalFetch = null;
|
|
}
|
|
if (originalEventSource && typeof window !== "undefined") {
|
|
window.EventSource = originalEventSource;
|
|
originalEventSource = null;
|
|
}
|
|
installCount = 0;
|
|
}
|
|
|
|
function rewriteInput(input: RequestInfo | URL, basePath: string): RequestInfo | URL {
|
|
if (typeof input === "string") {
|
|
return withBasePath(input, basePath);
|
|
}
|
|
if (typeof URL !== "undefined" && input instanceof URL) {
|
|
return new URL(withBasePath(input.href, basePath));
|
|
}
|
|
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
const rewritten = withBasePath(input.url, basePath);
|
|
if (rewritten === input.url) return input;
|
|
return new Request(rewritten, input);
|
|
}
|
|
return input;
|
|
}
|
|
|
|
/**
|
|
* Wrap `globalThis.fetch` (and `EventSource` in the browser) so absolute
|
|
* same-origin app paths receive the Next.js basePath prefix.
|
|
*
|
|
* No-op when basePath is empty (root deploys). Safe to call multiple times;
|
|
* uninstall when the last consumer unmounts.
|
|
*/
|
|
export function installBasePathFetch(
|
|
basePath: string = getDeployBasePath()
|
|
): () => void {
|
|
if (!basePath) return () => {};
|
|
if (typeof globalThis.fetch !== "function") return () => {};
|
|
|
|
if (installCount === 0) {
|
|
originalFetch = globalThis.fetch.bind(globalThis);
|
|
|
|
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
|
|
if (!originalFetch) return fetch(input, init);
|
|
return originalFetch(rewriteInput(input, basePath), init);
|
|
}) as typeof fetch;
|
|
|
|
if (typeof window !== "undefined" && typeof window.EventSource === "function") {
|
|
originalEventSource = window.EventSource;
|
|
const BaseES = originalEventSource;
|
|
|
|
// Subclass so `instanceof EventSource` and prototype methods keep working.
|
|
class BasePathEventSource extends BaseES {
|
|
constructor(url: string | URL, eventSourceInitDict?: EventSourceInit) {
|
|
const raw = typeof url === "string" ? url : url.toString();
|
|
super(withBasePath(raw, basePath), eventSourceInitDict);
|
|
}
|
|
}
|
|
|
|
window.EventSource = BasePathEventSource as typeof EventSource;
|
|
}
|
|
}
|
|
|
|
installCount++;
|
|
let active = true;
|
|
|
|
return () => {
|
|
if (!active) return;
|
|
active = false;
|
|
installCount = Math.max(0, installCount - 1);
|
|
if (installCount === 0) {
|
|
if (originalFetch) {
|
|
globalThis.fetch = originalFetch;
|
|
originalFetch = null;
|
|
}
|
|
if (originalEventSource && typeof window !== "undefined") {
|
|
window.EventSource = originalEventSource;
|
|
originalEventSource = null;
|
|
}
|
|
}
|
|
};
|
|
}
|