openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
594 lines (593 loc) • 26 kB
JavaScript
import { t as isPromiseLike } from "./promise-like-D7-l5Fsp.mjs";
import { t as cancelUnreadResponseBody } from "./http-response-body-umfRGZ-E.mjs";
import "./http-body-CgfQBa7S.mjs";
import { n as buildTimeoutAbortSignal } from "./fetch-timeout-BoUn0ffV.mjs";
import { f as isLoopbackIpAddress } from "./ip-jvtZlu_y.mjs";
import { t as captureGuardedFetchRequestAuthority } from "./fetch-request-authority-CdIOmi51.mjs";
import { i as logWarn } from "./logger-DskmrSVa.mjs";
import { n as normalizeHeadersInitForFetch, r as normalizeRequestInitHeadersForFetch } from "./fetch-headers-DD03wtQj.mjs";
import { t as normalizeHostname } from "./hostname-Db1ZcSGB.mjs";
import { t as getActiveManagedProxyLoopbackMode } from "./active-proxy-state-fUgx64cL.mjs";
import { s as shouldUseEnvHttpProxyForUrl } from "./proxy-env-DlRjxK5F.mjs";
import { c as resolveUndiciAutoSelectFamilyConnectOptions, n as createHttp1EnvHttpProxyAgent, r as createHttp1ProxyAgent, t as createHttp1Agent } from "./undici-runtime-DnsVgrv0.mjs";
import { i as createPinnedDispatcher, m as resolveSsrFPolicyForUrl, n as assertHostnameAllowedWithPolicy, p as resolvePinnedHostnameWithPolicy, r as closeDispatcher, t as SsrFBlockedError, u as matchesHostnameAllowlist } from "./ssrf-v45DW-yR.mjs";
import "./pinned-dispatcher-pool-BLmrDjeb.mjs";
import { i as isMockedFetch, n as fetchWithRuntimeDispatcher } from "./runtime-fetch-BUtybS_l.mjs";
import { o as globalUndiciStreamTimeoutMs } from "./undici-global-dispatcher-DH6sagj7.mjs";
//#region src/infra/net/configured-local-origin-bypass.ts
function parseHttpUrl(value) {
try {
const parsed = new URL(value.trim());
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
return parsed;
} catch {
return;
}
}
function isLoopbackManagedProxyBypassHost(hostname) {
const normalized = normalizeHostname(hostname);
return normalized === "localhost" || isLoopbackIpAddress(normalized);
}
/** Return whether proving a configured local-origin bypass requires target DNS. */
function shouldResolveConfiguredLocalOriginManagedProxyBypass(params) {
if (params.managedProxyBypass?.kind !== "configured-local-origin") return false;
const baseUrl = parseHttpUrl(params.managedProxyBypass.baseUrl);
if (!baseUrl || !isLoopbackManagedProxyBypassHost(baseUrl.hostname)) return false;
return parseHttpUrl(params.url.toString())?.origin.toLowerCase() === baseUrl.origin.toLowerCase();
}
/** Return whether a configured local provider origin may bypass the managed proxy. */
function shouldUseConfiguredLocalOriginManagedProxyBypass(params) {
if (!shouldResolveConfiguredLocalOriginManagedProxyBypass(params)) return false;
const loopbackMode = getActiveManagedProxyLoopbackMode();
if (loopbackMode === "proxy") return false;
if (loopbackMode === "block" && isLoopbackManagedProxyBypassHost(params.url.hostname)) throw new SsrFBlockedError("proxy: configured local provider loopback connections are blocked by proxy.loopbackMode");
return params.resolvedAddresses.length > 0 && params.resolvedAddresses.every((address) => isLoopbackIpAddress(address));
}
//#endregion
//#region src/infra/net/guarded-body-stream.ts
/**
* Shared body-stream cleanup for guarded fetch consumers (`fetchWithSsrFGuard`
* callers that re-wrap streaming responses).
*/
const guardedBodyCleanupRegistry = new FinalizationRegistry((held) => {
held.finalize().catch(() => void 0);
});
function attachBodyAbort(signal, controllerRef, cancel) {
const abort = () => {
const controller = controllerRef.deref();
if (!controller) return;
const reason = signal.reason ?? new DOMException("This operation was aborted", "AbortError");
const cleanup = cancel(reason);
controller.error(reason);
cleanup.catch(() => void 0);
};
if (signal.aborted) abort();
else signal.addEventListener("abort", abort, { once: true });
return () => signal.removeEventListener("abort", abort);
}
function wrapBodyStream(params, errorSource) {
const reader = params.body.getReader();
let finalized = false;
let abortAttached = false;
let detachAbort = () => {};
const cleanupRegistrationToken = {};
const finalize = async (cancelReader = async () => {
await reader.cancel().catch(() => void 0);
}) => {
if (finalized) return;
finalized = true;
detachAbort();
guardedBodyCleanupRegistry.unregister(cleanupRegistrationToken);
const [cancellation, readerRelease, cleanup] = await Promise.allSettled([
cancelReader(),
(async () => reader.releaseLock())(),
(async () => await params.cleanup())()
]);
if (cleanup.status === "rejected" && errorSource === "release") throw cleanup.reason;
if (readerRelease.status === "rejected") throw readerRelease.reason;
if (cancellation.status === "rejected" && errorSource === "cancellation") throw cancellation.reason;
};
const cancel = async (reason) => await finalize(async () => await reader.cancel(reason));
const wrappedBody = new ReadableStream({
async pull(controller) {
const signal = params.signal;
if (signal && !abortAttached) {
abortAttached = true;
detachAbort = attachBodyAbort(signal, new WeakRef(controller), cancel);
}
if (finalized) return;
try {
const chunk = await reader.read();
if (chunk.done) {
controller.close();
await finalize();
return;
}
params.refreshTimeout?.();
controller.enqueue(chunk.value);
} catch (error) {
if (finalized) return;
if (errorSource === "release") await finalize();
controller.error(error);
await finalize();
}
},
cancel
}, params.signal ? { highWaterMark: 0 } : void 0);
guardedBodyCleanupRegistry.register(wrappedBody, { finalize }, cleanupRegistrationToken);
return wrappedBody;
}
/** Wraps a guarded body with best-effort cleanup and explicit cancellation errors. */
function wrapGuardedBodyStream(params) {
return wrapBodyStream(params, "cancellation");
}
/** Preserves post-header request cancellation around runtime fetch body streams. */
function responseWithAbortSignal(response, signal) {
if (!response.body || !signal) return response;
const wrapped = new Response(wrapBodyStream({
body: response.body,
cleanup: () => {},
signal
}, "cancellation"), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
const applyMetadata = (target) => {
Object.defineProperties(target, {
headers: { value: response.headers },
url: { value: response.url },
redirected: { value: response.redirected },
type: { value: response.type },
clone: {
configurable: true,
value: () => applyMetadata(Response.prototype.clone.call(target)),
writable: true
}
});
return target;
};
return applyMetadata(wrapped);
}
const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
101,
103,
204,
205,
304
]);
/** Keeps request ownership through body completion, failure, or cancellation. */
function responseWithRelease(response, release) {
if (!response.body || NULL_BODY_STATUSES.has(response.status)) {
(async () => await release())();
return response;
}
return new Response(wrapBodyStream({
body: response.body,
cleanup: release
}, "release"), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
//#endregion
//#region src/infra/net/redirect-headers.ts
const CROSS_ORIGIN_REDIRECT_SAFE_HEADERS = /* @__PURE__ */ new Set([
"accept",
"accept-encoding",
"accept-language",
"cache-control",
"content-language",
"content-type",
"if-match",
"if-modified-since",
"if-none-match",
"if-unmodified-since",
"pragma",
"range",
"user-agent"
]);
/**
* Keeps only headers that are safe to replay after a redirect crosses origins.
* Authorization/cookie-like metadata must be dropped before the follow-up fetch.
*/
function retainSafeHeadersForCrossOriginRedirect$1(headers) {
if (!headers) return headers;
const incoming = new Headers(normalizeHeadersInitForFetch(headers));
const safeHeaders = {};
for (const [key, value] of incoming.entries()) if (CROSS_ORIGIN_REDIRECT_SAFE_HEADERS.has(key)) safeHeaders[key] = value;
return safeHeaders;
}
//#endregion
//#region src/infra/net/fetch-guard.ts
const GUARDED_FETCH_MODE = {
STRICT: "strict",
TRUSTED_ENV_PROXY: "trusted_env_proxy",
TRUSTED_EXPLICIT_PROXY: "trusted_explicit_proxy"
};
var GuardedFetchRedirectError = class extends Error {
constructor(params) {
super(`Too many redirects (limit: ${params.maxRedirects})`);
this.name = "GuardedFetchRedirectError";
this.status = params.status;
this.maxRedirects = params.maxRedirects;
}
};
const DEFAULT_MAX_REDIRECTS = 3;
const OPENCLAW_DEBUG_PROXY_ENABLED = "OPENCLAW_DEBUG_PROXY_ENABLED";
function getRedirectVisitKey(url, init) {
return `${init?.method?.toUpperCase() ?? "GET"} ${url}`;
}
function isTruthyEnvValue(value) {
return value === "1" || value === "true" || value === "yes" || value === "on";
}
function withStrictGuardedFetchMode(params) {
return {
...params,
mode: GUARDED_FETCH_MODE.STRICT
};
}
function withTrustedEnvProxyGuardedFetchMode(params) {
return {
...params,
mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY
};
}
function withTrustedExplicitProxyGuardedFetchMode(params) {
return {
...params,
mode: GUARDED_FETCH_MODE.TRUSTED_EXPLICIT_PROXY
};
}
function resolveGuardedFetchMode(params) {
if (params.mode) return params.mode;
if (params.proxy === "env" && params.dangerouslyAllowEnvProxyWithoutPinnedDns === true) return GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY;
return GUARDED_FETCH_MODE.STRICT;
}
function isManagedProxyActive() {
return process.env["OPENCLAW_PROXY_ACTIVE"] === "1";
}
function assertExplicitProxySupportsPinnedDns(url, dispatcherPolicy, pinDns) {
if (pinDns !== false && dispatcherPolicy?.mode === "explicit-proxy" && url.protocol !== "https:") throw new Error("Explicit proxy SSRF pinning requires HTTPS targets; plain HTTP targets are not supported");
}
function createPolicyDispatcherWithoutPinnedDns(dispatcherPolicy, timeoutMs) {
if (!dispatcherPolicy) return null;
if (dispatcherPolicy.mode === "direct") return createHttp1Agent(dispatcherPolicy.connect ? { connect: { ...dispatcherPolicy.connect } } : void 0, timeoutMs);
if (dispatcherPolicy.mode === "env-proxy") return createHttp1EnvHttpProxyAgent({
...dispatcherPolicy.connect ? { connect: { ...dispatcherPolicy.connect } } : {},
...dispatcherPolicy.proxyTls ? { proxyTls: { ...dispatcherPolicy.proxyTls } } : {}
}, timeoutMs);
const proxyUrl = dispatcherPolicy.proxyUrl.trim();
const requestTls = dispatcherPolicy.proxyTls;
return createHttp1ProxyAgent({
uri: proxyUrl,
...requestTls ? { requestTls: { ...requestTls } } : {}
}, timeoutMs);
}
async function assertExplicitProxyAllowed(dispatcherPolicy, lookupFn, policy, signal, trustedProxy) {
if (!dispatcherPolicy || dispatcherPolicy.mode !== "explicit-proxy") return;
let parsedProxyUrl;
try {
parsedProxyUrl = new URL(dispatcherPolicy.proxyUrl);
} catch {
throw new Error("Invalid explicit proxy URL");
}
const trustedSocks = trustedProxy && ["socks:", "socks5:"].includes(parsedProxyUrl.protocol);
if (!["http:", "https:"].includes(parsedProxyUrl.protocol) && !trustedSocks) throw new Error("Explicit proxy URL must use http or https");
const proxyPolicy = policy || dispatcherPolicy.allowPrivateProxy === true ? {
...policy,
hostnameAllowlist: void 0,
...dispatcherPolicy.allowPrivateProxy === true ? { allowPrivateNetwork: true } : {}
} : void 0;
await resolvePinnedHostnameWithPolicy(parsedProxyUrl.hostname, {
lookupFn,
policy: proxyPolicy,
signal
});
}
function isRedirectStatus(status) {
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
}
function isAmbientGlobalFetch(params) {
return typeof params.fetchImpl === "function" && typeof params.globalFetch === "function" && params.fetchImpl === params.globalFetch;
}
async function prepareGuardedFetchCapture(params, fetchImpl) {
if (params.capture === false || !isTruthyEnvValue(process.env[OPENCLAW_DEBUG_PROXY_ENABLED])) return { fetchImpl };
const { prepareHttpCaptureForTransport, resolveDebugProxyFetchTransport } = await import("./runtime-2lyKvCKF.mjs");
return {
fetchImpl: resolveDebugProxyFetchTransport(fetchImpl),
capture: prepareHttpCaptureForTransport()
};
}
function retainSafeHeadersForCrossOriginRedirect(init) {
if (!init?.headers) return init;
return {
...init,
headers: retainSafeHeadersForCrossOriginRedirect$1(init.headers)
};
}
function resolveRetainedAuthorizationForRedirect(params) {
const init = params.init;
if (!init?.headers || !params.hostnameAllowlist?.length) return;
if (params.nextUrl.protocol !== "https:") return;
if (!params.hostnameAllowlist.includes("*") && !matchesHostnameAllowlist(params.nextUrl.hostname, params.hostnameAllowlist)) return;
const normalizedInit = normalizeRequestInitHeadersForFetch(init);
if (!normalizedInit?.headers) return;
return new Headers(normalizedInit.headers).get("authorization") ?? void 0;
}
function restoreRedirectAuthorization(params) {
if (!params.authorization) return params.init;
const headers = new Headers(params.init?.headers);
headers.set("Authorization", params.authorization);
return {
...params.init,
headers
};
}
function dropBodyHeaders(headers) {
if (!headers) return headers;
const nextHeaders = new Headers(normalizeHeadersInitForFetch(headers));
nextHeaders.delete("content-encoding");
nextHeaders.delete("content-language");
nextHeaders.delete("content-length");
nextHeaders.delete("content-location");
nextHeaders.delete("content-type");
nextHeaders.delete("transfer-encoding");
return nextHeaders;
}
function rewriteRedirectInit(params) {
const { init, status } = params;
if (!init) return init;
const currentMethod = init.method?.toUpperCase() ?? "GET";
const shouldForceGet = status === 303 ? currentMethod !== "GET" && currentMethod !== "HEAD" : (status === 301 || status === 302) && currentMethod === "POST";
const shouldDropUnsafeBody = params.crossOrigin && !params.allowUnsafeReplay && currentMethod !== "GET" && currentMethod !== "HEAD";
if (!shouldForceGet && !shouldDropUnsafeBody) return init;
return {
...init,
...shouldForceGet ? { method: "GET" } : {},
body: void 0,
headers: dropBodyHeaders(init.headers)
};
}
async function fetchWithSsrFGuard(params) {
const { managedProxyBypass: _ignoredManagedProxyBypass, ...publicParams } = params;
return await fetchWithSsrFGuardInternal(publicParams);
}
async function fetchConfiguredLocalOriginWithSsrFGuard({ configuredLocalOriginBaseUrl, ...params }) {
return await fetchWithSsrFGuardInternal({
...params,
managedProxyBypass: {
kind: "configured-local-origin",
baseUrl: configuredLocalOriginBaseUrl
}
});
}
async function fetchWithSsrFGuardInternal(params) {
const assertCurrent = captureGuardedFetchRequestAuthority();
if (params.allowCrossOriginUnsafeRedirectReplay === true && params.rejectCrossOriginUnsafeRedirectReplay === true) throw new TypeError("Cross-origin unsafe redirect replay cannot be both allowed and rejected");
const globalFetch = globalThis.fetch;
const defaultFetch = params.fetchImpl ?? globalFetch;
if (!defaultFetch) throw new Error("fetch is not available");
const isUsingMockedFetch = isMockedFetch(defaultFetch);
const supportsDispatcherInit = params.fetchImpl !== void 0 && !isAmbientGlobalFetch({
fetchImpl: params.fetchImpl,
globalFetch
}) || isUsingMockedFetch;
const captureAdmission = await prepareGuardedFetchCapture(params, defaultFetch);
const maxRedirects = typeof params.maxRedirects === "number" && Number.isFinite(params.maxRedirects) ? Math.max(0, Math.floor(params.maxRedirects)) : DEFAULT_MAX_REDIRECTS;
const mode = resolveGuardedFetchMode(params);
const { signal, cleanup, refresh } = buildTimeoutAbortSignal({
timeoutMs: params.timeoutMs,
signal: params.signal ?? params.init?.signal ?? void 0,
operation: "fetchWithSsrFGuard",
url: params.url
});
let finished = false;
const finishRequest = async (releaseDispatcher) => {
if (finished) return;
finished = true;
cleanup();
await releaseDispatcher?.();
};
let currentUrl = params.url;
let currentInit = normalizeRequestInitHeadersForFetch(params.init ? { ...params.init } : void 0);
const visited = /* @__PURE__ */ new Set([getRedirectVisitKey(currentUrl, currentInit)]);
let redirectCount = 0;
while (true) {
let parsedUrl;
try {
parsedUrl = new URL(currentUrl);
} catch {
await finishRequest();
throw new Error("Invalid URL: must be http or https");
}
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
await finishRequest();
throw new Error("Invalid URL: must be http or https");
}
if (params.requireHttps === true && parsedUrl.protocol !== "https:") {
await finishRequest();
throw new Error("URL must use https");
}
let dispatcher = null;
let dispatcherLease;
let response;
const requestController = new AbortController();
const releaseDispatcher = async () => {
requestController.abort();
await cancelUnreadResponseBody(response);
await (dispatcherLease ? dispatcherLease.release() : closeDispatcher(dispatcher));
};
const policyForUrl = resolveSsrFPolicyForUrl(parsedUrl, params.policy);
const dispatcherPolicy = params.resolveDispatcherPolicy?.(parsedUrl) ?? params.dispatcherPolicy;
const resolvePinnedHostname = async () => await resolvePinnedHostnameWithPolicy(parsedUrl.hostname, {
lookupFn: params.lookupFn,
policy: policyForUrl,
signal
});
try {
const usesTrustedExplicitProxyMode = mode === GUARDED_FETCH_MODE.TRUSTED_EXPLICIT_PROXY && dispatcherPolicy?.mode === "explicit-proxy";
assertExplicitProxySupportsPinnedDns(parsedUrl, dispatcherPolicy, usesTrustedExplicitProxyMode ? false : params.pinDns);
await assertExplicitProxyAllowed(dispatcherPolicy, params.lookupFn, params.policy, signal, usesTrustedExplicitProxyMode);
const isStrictManagedProxyActive = mode === GUARDED_FETCH_MODE.STRICT && isManagedProxyActive();
const shouldCheckManagedProxyBypass = isStrictManagedProxyActive && shouldResolveConfiguredLocalOriginManagedProxyBypass({
url: parsedUrl,
managedProxyBypass: params.managedProxyBypass
});
const canUseManagedProxy = isStrictManagedProxyActive && (shouldUseEnvHttpProxyForUrl(parsedUrl.toString()) || shouldCheckManagedProxyBypass);
const canUseTrustedEnvProxy = (mode === GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY || params.useEnvProxyForEligibleUrls === true && !canUseManagedProxy) && !dispatcherPolicy && shouldUseEnvHttpProxyForUrl(parsedUrl.toString());
const canUseMockedFetchWithoutDns = isUsingMockedFetch && params.lookupFn === void 0 && !canUseTrustedEnvProxy && !canUseManagedProxy && !usesTrustedExplicitProxyMode && params.pinDns !== false;
const timeoutMs = params.timeoutMs !== void 0 ? params.timeoutMs : globalUndiciStreamTimeoutMs;
if (canUseTrustedEnvProxy || canUseManagedProxy || params.pinDns === false) assertHostnameAllowedWithPolicy(parsedUrl.hostname, policyForUrl);
if (canUseTrustedEnvProxy) dispatcher = createHttp1EnvHttpProxyAgent(void 0, timeoutMs);
else if (canUseManagedProxy) {
if (shouldCheckManagedProxyBypass) {
const pinned = await resolvePinnedHostname();
dispatcher = shouldUseConfiguredLocalOriginManagedProxyBypass({
url: parsedUrl,
managedProxyBypass: params.managedProxyBypass,
resolvedAddresses: pinned.addresses
}) ? createPinnedDispatcher(pinned, dispatcherPolicy, policyForUrl, timeoutMs) : createHttp1EnvHttpProxyAgent({
noProxy: "",
...dispatcherPolicy?.mode === "direct" && dispatcherPolicy.connect ? { requestTls: { ...dispatcherPolicy.connect } } : {}
}, timeoutMs);
} else dispatcher = createHttp1EnvHttpProxyAgent(void 0, timeoutMs);
} else if (usesTrustedExplicitProxyMode) {
assertHostnameAllowedWithPolicy(parsedUrl.hostname, policyForUrl);
dispatcher = createPolicyDispatcherWithoutPinnedDns(dispatcherPolicy, timeoutMs);
} else if (canUseMockedFetchWithoutDns) assertHostnameAllowedWithPolicy(parsedUrl.hostname, policyForUrl);
else if (params.pinDns === false) {
await resolvePinnedHostname();
dispatcher = createPolicyDispatcherWithoutPinnedDns(dispatcherPolicy, timeoutMs);
} else {
const pinned = await resolvePinnedHostname();
if (params.dispatcherPool && mode === GUARDED_FETCH_MODE.STRICT && dispatcherPolicy === void 0) {
const familyConnect = resolveUndiciAutoSelectFamilyConnectOptions();
const key = JSON.stringify({
origin: parsedUrl.origin,
addresses: [...pinned.addresses].toSorted(),
timeoutMs: timeoutMs ?? null,
familyConnect: familyConnect ?? null,
policy: policyForUrl ?? null
});
dispatcherLease = params.dispatcherPool.acquire({
key,
groupKey: parsedUrl.origin,
createDispatcher: () => createPinnedDispatcher(pinned, familyConnect ? {
mode: "direct",
connect: familyConnect
} : void 0, policyForUrl, timeoutMs)
});
dispatcher = dispatcherLease ? dispatcherLease.dispatcher : createPinnedDispatcher(pinned, void 0, policyForUrl, timeoutMs);
} else dispatcher = createPinnedDispatcher(pinned, dispatcherPolicy, policyForUrl, timeoutMs);
}
const init = {
...currentInit ? { ...currentInit } : {},
redirect: "manual",
...dispatcher ? { dispatcher } : {},
signal: signal ? AbortSignal.any([signal, requestController.signal]) : requestController.signal
};
const shouldUseRuntimeFetch = Boolean(dispatcher) && !supportsDispatcherInit;
const beforeRequestResult = params.beforeRequest?.();
if (isPromiseLike(beforeRequestResult)) {
Promise.resolve(beforeRequestResult).catch(() => void 0);
throw new TypeError("beforeRequest must be synchronous.");
}
assertCurrent?.();
const captureParams = {
url: parsedUrl.toString(),
method: currentInit?.method ?? "GET",
signal: process.versions.bun ? init.signal ?? void 0 : void 0,
requestHeaders: currentInit?.headers,
requestBody: currentInit?.body ?? null,
transport: "http",
flowId: params.capture === false ? void 0 : params.capture?.flowId,
meta: {
captureOrigin: "guarded-fetch",
...params.auditContext ? { auditContext: params.auditContext } : {},
...params.capture === false ? {} : params.capture?.meta,
...params.capture && params.capture.sensitiveRequestHeaderNames ? { sensitiveRequestHeaderNames: params.capture.sensitiveRequestHeaderNames } : {}
}
};
try {
response = shouldUseRuntimeFetch ? await fetchWithRuntimeDispatcher(parsedUrl.toString(), init) : await captureAdmission.fetchImpl(parsedUrl.toString(), init);
} catch (error) {
captureAdmission.capture?.({
...captureParams,
error
});
throw error;
}
params.onResponse?.(response.status);
captureAdmission.capture?.({
...captureParams,
response
});
if (isRedirectStatus(response.status)) {
redirectCount += 1;
if (redirectCount > maxRedirects) throw new GuardedFetchRedirectError({
status: response.status,
maxRedirects
});
const location = response.headers.get("location");
if (!location) throw new Error(`Redirect missing location header (${response.status})`);
const nextParsedUrl = new URL(location, parsedUrl);
const nextUrl = nextParsedUrl.toString();
const retainedAuthorization = resolveRetainedAuthorizationForRedirect({
init: currentInit,
nextUrl: nextParsedUrl,
hostnameAllowlist: params.retainAuthorizationRedirectHostnameAllowlist
});
const crossOrigin = nextParsedUrl.origin !== parsedUrl.origin;
const methodRedirectInit = rewriteRedirectInit({
init: currentInit,
status: response.status,
crossOrigin: false,
allowUnsafeReplay: true
});
if (crossOrigin) {
const redirectedMethod = methodRedirectInit?.method?.toUpperCase() ?? "GET";
const redirectedBody = methodRedirectInit?.body;
if (params.rejectCrossOriginUnsafeRedirectReplay === true && redirectedMethod !== "GET" && redirectedMethod !== "HEAD" && redirectedBody != null) throw new Error(`Refusing to follow cross-origin redirect for ${redirectedMethod} request body (${parsedUrl.origin} -> ${nextParsedUrl.origin})`);
}
currentInit = rewriteRedirectInit({
init: methodRedirectInit,
status: response.status,
crossOrigin,
allowUnsafeReplay: params.allowCrossOriginUnsafeRedirectReplay === true
});
if (crossOrigin) {
currentInit = retainSafeHeadersForCrossOriginRedirect(currentInit);
currentInit = restoreRedirectAuthorization({
init: currentInit,
authorization: retainedAuthorization
});
}
const nextVisitKey = getRedirectVisitKey(nextUrl, currentInit);
if (visited.has(nextVisitKey)) throw new Error("Redirect loop detected");
visited.add(nextVisitKey);
await releaseDispatcher();
currentUrl = nextUrl;
continue;
}
return {
response: process.versions.bun && !(response instanceof Response) ? responseWithAbortSignal(response, init.signal ?? void 0) : response,
finalUrl: currentUrl,
release: async () => finishRequest(releaseDispatcher),
refreshTimeout: refresh,
dispatcherReused: dispatcherLease?.reused
};
} catch (err) {
if (err instanceof SsrFBlockedError) {
const context = params.auditContext ?? "url-fetch";
logWarn(`security: blocked URL fetch (${context}) targetOrigin=${parsedUrl.origin} reason=${err.message}`);
}
await finishRequest(releaseDispatcher);
throw err;
}
}
}
//#endregion
export { isManagedProxyActive as a, withTrustedExplicitProxyGuardedFetchMode as c, wrapGuardedBodyStream as d, fetchWithSsrFGuard as i, retainSafeHeadersForCrossOriginRedirect$1 as l, GuardedFetchRedirectError as n, withStrictGuardedFetchMode as o, fetchConfiguredLocalOriginWithSsrFGuard as r, withTrustedEnvProxyGuardedFetchMode as s, GUARDED_FETCH_MODE as t, responseWithRelease as u };