different-ai/openwork · error · RemoteMcpAppError

source_fetch_failed

source_fetch_failed

Error message

${error instanceof Error ? error.message : "The app URL could not be downloaded."}

What it means

fetchRemoteMcpApp wraps the outbound guardedFetch download in a try/catch. Any failure that is not already a RemoteMcpAppError (network failure, DNS error, timeout from AbortSignal.timeout, TLS error, invalid URL behavior) is rethrown as a 502 source_fetch_failed with the underlying error message, or a generic fallback message.

Source

Thrown at ee/apps/den-api/src/remote-mcp-apps.ts:292

    return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes)
  } catch {
    throw new RemoteMcpAppError(422, "invalid_encoding", "Remote MCP Apps must be UTF-8 HTML.")
  }
}

export async function fetchRemoteMcpApp(sourceUrl: string) {
  const { env } = await import("./env.js")
  const normalizedUrl = validateRemoteMcpAppSourceUrl(sourceUrl, env.allowPrivateMcpUrls)
  const guardedFetch = env.allowPrivateMcpUrls ? createRealmSafeFetch() : createGuardedFetch()
  let response: Response
  try {
    response = await guardedFetch(normalizedUrl, {
      headers: { accept: "text/html,application/xhtml+xml;q=0.9,text/plain;q=0.5" },
      signal: AbortSignal.timeout(REMOTE_MCP_APP_FETCH_TIMEOUT_MS),
    })
  } catch (error) {
    if (error instanceof RemoteMcpAppError) throw error
    throw new RemoteMcpAppError(502, "source_fetch_failed", error instanceof Error ? error.message : "The app URL could not be downloaded.")
  }
  if (!response.ok) {
    await response.body?.cancel()
    throw new RemoteMcpAppError(502, "source_fetch_failed", `The app URL returned HTTP ${response.status}.`)
  }
  const contentType = response.headers.get("content-type")
  try {
    validateRemoteMcpAppContentType(contentType)
  } catch (error) {
    await response.body?.cancel()
    throw error
  }
  const html = await boundedResponseText(response)
  const inspected = inspectRemoteMcpAppHtml(html)
  const resolvedSourceUrl = validateRemoteMcpAppSourceUrl(response.url || normalizedUrl, env.allowPrivateMcpUrls)
  return {
    ...inspected,
    html,

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Check the source URL is reachable: curl it from the den-api host and fix the hostname/DNS.
  2. Re-register the app with the correct, publicly reachable URL.
  3. If the origin is legitimately slow, reduce the page size or check for network issues; the timeout is REMOTE_MCP_APP_FETCH_TIMEOUT_MS.
  4. For internal/private URLs, enable allowPrivateMcpUrls in the environment configuration.
  5. Fix TLS (valid certificate chain) on the source server.
Defensive patterns

Strategy: retry

Validate before calling

const probe = await fetch(sourceUrl, { method: "HEAD" }).catch((e) => { throw new Error(`Unreachable before import: ${e.message}`); });
if (!probe.ok) throw new Error(`Probe failed with HTTP ${probe.status}`);

Try / catch

try {
  await importRemoteMcpApp({ sourceUrl, ... });
} catch (e) {
  if (e instanceof RemoteMcpAppError && e.code === "source_fetch_failed") {
    // inspect e.message for the underlying cause; retry with backoff for transient network errors
  } else throw e;
}

Prevention

When it happens

Trigger: guardedFetch throws while fetching the validated sourceUrl: connection refused/reset, DNS resolution failure, TLS certificate errors, or the fetch exceeding REMOTE_MCP_APP_FETCH_TIMEOUT_MS via AbortSignal.timeout.

Common situations: Typo in the source URL hostname; source server temporarily down; private-network URL blocked (SSRF guard) or allowPrivateMcpUrls disabled; slow origin exceeding the fetch timeout; self-signed certificates.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/d9dbd8549a2d004b. Report an issue: GitHub.