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

  1. Surface the returned finalUrl to the user for explicit domain approval, then re-fetch the approved URL directly.
  2. Pre-resolve short links (follow once via a resolver) before calling fetchUrl.
  3. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/0f9bce3e1f7bc46c. Report an issue: GitHub.