n8n-io/n8n · warning · CrossHostRedirectError
Redirect to a different host: ${finalUrl}
Error message
Redirect to a different host: ${finalUrl} What it means
fetchUrl (tools/utils/web-fetch.utils.ts) follows redirects up to WEB_FETCH_MAX_REDIRECTS, but its beforeRedirect hook compares each hop's host to the original URL's host. On a host mismatch it throws CrossHostRedirectError so the caller can run human-in-the-loop domain approval before following the redirect. This is the SSRF guard's cross-host halt, not a hard network failure.
Source
Thrown at packages/@n8n/ai-workflow-builder.ee/src/tools/utils/web-fetch.utils.ts:171
ssrf: SsrfGuard,
signal?: AbortSignal,
): Promise<FetchResult> {
// Pre-flight: reject before opening any connection.
const preflight = await ssrf.validateUrl(url);
if (!preflight.ok) return { status: 'blocked' };
const originalHost = normalizeHost(url);
const config: AxiosRequestConfig = {
// Honored by Node's http(s) agent on every hop; validates the resolved IP at
// connect time, preventing DNS-rebinding TOCTOU.
lookup: ssrf.createSecureLookup() as AxiosRequestConfig['lookup'],
beforeRedirect: (opts: Record<string, string>) => {
// Direct-IP redirect targets do no DNS lookup, so validate them here first.
ssrf.validateRedirectSync(opts.href);
// Halt auto-follow on cross-host redirects so the caller can run HITL approval.
if (normalizeHost(opts.href) !== originalHost) {
throw new CrossHostRedirectError(opts.href);
}
},
maxRedirects: WEB_FETCH_MAX_REDIRECTS,
timeout: WEB_FETCH_TIMEOUT_MS,
signal,
responseType: 'stream',
// Let the manual byte-cap own truncation; arraybuffer + maxContentLength would reject.
maxContentLength: Infinity,
maxBodyLength: Infinity,
validateStatus: () => true,
headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'User-Agent': 'n8n-workflow-builder/1.0',
Accept: 'text/html,application/xhtml+xml,*/*',
},
};
let response;View on GitHub (pinned to 5ac6606e81)
Solutions
- Surface the returned finalUrl to the user for explicit domain approval, then re-fetch the approved URL directly.
- Pre-resolve short links (follow once via a resolver) before calling fetchUrl.
- If the cross-host target is known-safe, pass it as the original URL so it becomes the baseline host.
Example fix
// before: short link crosses host on redirect
const r = await fetchUrl('https://t.co/abc', ssrf);
// r.status === 'redirect_new_host', r.finalUrl set
// after: user approves finalUrl, fetch it directly
const r = await fetchUrl(approvedFinalUrl, ssrf); Defensive patterns
Strategy: validation
Validate before calling
// Resolve redirects once yourself (or check the short-link) before calling fetchUrl,
// so the approved final URL becomes the baseline host.
async function preResolve(url: string): Promise<string> {
// one-hop manual check; if host changes, surface to the user for approval
return approvedUrl;
}
const finalUrl = await preResolve(userUrl);
const result = await fetchUrl(finalUrl, ssrf); Type guard
function isCrossHostRedirectResult(r: unknown): r is { status: 'redirect_new_host'; finalUrl: string } {
return typeof r === 'object' && r !== null && (r as any).status === 'redirect_new_host';
} Try / catch
// fetchUrl does not throw on cross-host; it returns a discriminated status.
const result = await fetchUrl(url, ssrf);
if (result.status === 'redirect_new_host') {
// surface result.finalUrl to the user for HITL approval, then re-fetch it.
} Prevention
- Pre-resolve short links so the user approves the final host before fetchUrl runs.
- Keep a domain allowlist; auto-approve known-safe redirects and prompt for the rest.
- Treat redirect_new_host as an approval checkpoint, not an error.
When it happens
Trigger: Any web fetch where a redirect (3xx) crosses to a different hostname than the original URL: t.co/bit.ly short links, CDN domains, http-to-https host rewrites, login redirects to an IdP, affiliate hops.
Common situations: User pastes a short URL; site redirects to a www/alternate domain for content; login-walled pages redirect to an auth host; regional CDN redirect.
Related errors
- Webhook URL must use HTTPS. Got: ${url.protocol}
- Webhook URL cannot target localhost
- Webhook URL cannot target private/internal IP addresses
- Webhook URL cannot target internal hostname: ${hostname}
- Webhook URL hostname resolves to a private/internal IP addre
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/0f9bce3e1f7bc46c.
Report an issue: GitHub.