Skip to content
Open
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
125 changes: 121 additions & 4 deletions app/L0/_all/mod/_core/framework/js/fetch-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ const FETCH_PROXY_MARKER = Symbol.for("space.fetch-proxy-installed");
const RETRYABLE_STATE_SYNC_ERROR = "Server state is still synchronizing. Retry the request.";
const STATE_SYNC_RETRY_DELAY_MS = 100;
const STATE_SYNC_RETRY_MAX_ATTEMPTS = 3;
const proxyFallbackOrigins = new Set();
// Persist the proxy-fallback origin set in localStorage so a known-CORS-blocked
// origin (e.g. `https://chatgpt.com` for the OpenAI Codex provider, the
// inference-API host of a polling status widget) routes directly through
// `/api/proxy` from the *first* fetch of every page load instead of paying
// one failed-direct-fetch + DevTools-console error per session start.
// Stale entries are dropped after 7 days to let recovered upstreams (a service
// that finally adds `Access-Control-Allow-Origin`) leave the cache naturally.
const FETCH_PROXY_FALLBACK_ORIGINS_STORAGE_KEY = "space.framework.proxy-fallback-origins";
const FETCH_PROXY_FALLBACK_ORIGIN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const proxyFallbackOrigins = new Map();

function requestCanHaveBody(method) {
return !["GET", "HEAD"].includes(String(method || "GET").toUpperCase());
Expand Down Expand Up @@ -44,14 +53,119 @@ function getProxyFallbackOriginKey(targetUrl) {
return new URL(targetUrl, window.location.href).origin;
}

function getProxyFallbackOriginStorage() {
try {
return window.localStorage;
} catch {
// Some sandboxed contexts throw on `window.localStorage` access. Falling
// back to in-memory only is acceptable here — origin caching is an
// optimisation, not a correctness requirement.
return null;
}
}

function loadPersistedProxyFallbackOrigins() {
const storage = getProxyFallbackOriginStorage();

if (!storage) {
return;
}

let raw;
try {
raw = storage.getItem(FETCH_PROXY_FALLBACK_ORIGINS_STORAGE_KEY);
} catch {
return;
}

if (!raw) {
return;
}

let parsed;
try {
parsed = JSON.parse(raw);
} catch {
// Corrupt entry: clear it so we don't repeatedly try to parse the same
// garbage on every page load.
try {
storage.removeItem(FETCH_PROXY_FALLBACK_ORIGINS_STORAGE_KEY);
} catch {}
return;
}

if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return;
}

const now = Date.now();
Object.entries(parsed).forEach(([origin, lastSeenAt]) => {
if (typeof origin !== "string" || !origin) {
return;
}

const timestamp = Number.isFinite(Number(lastSeenAt)) ? Number(lastSeenAt) : 0;

if (timestamp <= 0 || now - timestamp > FETCH_PROXY_FALLBACK_ORIGIN_TTL_MS) {
return;
}

proxyFallbackOrigins.set(origin, timestamp);
});
}

function persistProxyFallbackOrigins() {
const storage = getProxyFallbackOriginStorage();

if (!storage) {
return;
}

if (proxyFallbackOrigins.size === 0) {
try {
storage.removeItem(FETCH_PROXY_FALLBACK_ORIGINS_STORAGE_KEY);
} catch {}
return;
}

const payload = Object.fromEntries(proxyFallbackOrigins.entries());

try {
storage.setItem(FETCH_PROXY_FALLBACK_ORIGINS_STORAGE_KEY, JSON.stringify(payload));
} catch {
// Quota / disabled-storage failures are non-fatal: the in-memory Map
// still serves the rest of the page lifetime.
}
}

function hasProxyFallbackOrigin(targetUrl) {
return proxyFallbackOrigins.has(getProxyFallbackOriginKey(targetUrl));
const origin = getProxyFallbackOriginKey(targetUrl);
const lastSeenAt = proxyFallbackOrigins.get(origin);

if (!Number.isFinite(lastSeenAt)) {
return false;
}

if (Date.now() - lastSeenAt > FETCH_PROXY_FALLBACK_ORIGIN_TTL_MS) {
// TTL expired since the last touch on this origin. Drop it and force the
// next fetch to re-test the upstream directly so a recovered server
// (one that finally added `Access-Control-Allow-Origin`) can leave the
// cache naturally.
proxyFallbackOrigins.delete(origin);
persistProxyFallbackOrigins();
return false;
}

return true;
}

function rememberProxyFallbackOrigin(targetUrl) {
proxyFallbackOrigins.add(getProxyFallbackOriginKey(targetUrl));
proxyFallbackOrigins.set(getProxyFallbackOriginKey(targetUrl), Date.now());
persistProxyFallbackOrigins();
}

loadPersistedProxyFallbackOrigins();

function requestSupportsProxyFallback(request) {
const mode = String(request.mode || "cors").toLowerCase();
return !["no-cors", "same-origin"].includes(mode);
Expand Down Expand Up @@ -209,7 +323,10 @@ export function installFetchProxy(options = {}) {
proxiedFetch.originalFetch = originalFetch;
proxiedFetch.hasProxyFallbackOrigin = hasProxyFallbackOrigin;
proxiedFetch.rememberProxyFallbackOrigin = rememberProxyFallbackOrigin;
proxiedFetch.clearProxyFallbackOrigins = () => proxyFallbackOrigins.clear();
proxiedFetch.clearProxyFallbackOrigins = () => {
proxyFallbackOrigins.clear();
persistProxyFallbackOrigins();
};
proxiedFetch[FETCH_PROXY_MARKER] = true;

window.fetch = proxiedFetch;
Expand Down