ruvnet/ruflo · error · Error

MCP server "${server.name}" returned HTTP ${httpStatus}: ${h

Error message

MCP server "${server.name}" returned HTTP ${httpStatus}: ${httpErr instanceof Error ? httpErr.message : String(httpErr)}

What it means

Thrown by getClient when the StreamableHTTP connect fails with a definitive non-429 HTTP status in 400-599 (excluding 408). After recordFailure stores the cooldown, the pool throws a generic Error: 'MCP server "<name>" returned HTTP <status>: <message>', attaching the original error as cause. SSE fallback is intentionally skipped because a 4xx/5xx response from the streamable endpoint will fail identically over SSE.

Source

Thrown at ruflo/src/ruvocal/src/lib/server/mcp/clientPool.ts:164

		// 4xx/5xx (except 408): falling back to SSE will hit the same upstream
		// and fail the same way. Surface a clean error with rate-limit info.
		if (shouldSkipSseFallback(httpStatus)) {
			try {
				await client.close?.();
			} catch {}

			recordFailure(key, httpStatus, httpErr);

			if (httpStatus === 429) {
				const retryAfterMs = extractRetryAfterMs(httpErr);
				throw new McpRateLimitedError(
					server.name,
					429,
					retryAfterMs,
					httpErr instanceof Error ? httpErr.message : String(httpErr)
				);
			}
			throw new Error(
				`MCP server "${server.name}" returned HTTP ${httpStatus}: ` +
					(httpErr instanceof Error ? httpErr.message : String(httpErr)),
				{ cause: httpErr instanceof Error ? httpErr : undefined }
			);
		}

		// Recoverable failure (network, 408, transport mismatch) — try SSE fallback.
		try {
			await client.connect(new SSEClientTransport(url, { requestInit }));
		} catch (err) {
			try {
				await client.close?.();
			} catch {}

			// Combine both errors for the caller. Honor 429 status if either side
			// surfaced a rate-limit (the upstream is rate-limiting independent of
			// transport).
			const sseStatus = statusFromTransportError(err);

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. For 401/403: add or refresh McpServerConfig.headers Authorization.
  2. For 404: verify the MCP server URL and path (often /mcp or /sse).
  3. For 5xx: confirm upstream health, then wait out the 5s cooldown (or evictFromPool) before retrying.
  4. Inspect err.cause for the SDK's original transport error for more detail.

Example fix

// before
const client = await getClient({ url: mcpUrl, name: 'svc', headers: {} });

// after
try {
  const client = await getClient({
    url: mcpUrl,
    name: 'svc',
    headers: { Authorization: `Bearer ${token}` },
  });
} catch (e) {
  if (e instanceof Error && /returned HTTP 401|403/.test(e.message)) {
    await refreshToken();
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: a HEAD/GET probe is NOT recommended (it also consumes quota).
// Instead, validate config shape before calling getClient:
function validateServerConfig(server: McpServerConfig): string[] {
  const issues: string[] = [];
  try { new URL(server.url); } catch { issues.push("invalid-url"); }
  if (/401|403/.test(String(server.url)) || !server.headers?.Authorization) {
    // warn, not block
  }
  return issues;
}

Type guard

function isHttpFailureFor(e: unknown, statusRe: RegExp): e is Error {
  return e instanceof Error && new RegExp(`returned HTTP ${statusRe.source}`).test(e.message);
}

Try / catch

try { const client = await getClient(server); }
catch (e) {
  if (e instanceof Error && /returned HTTP 401|403/.test(e.message)) {
    await refreshMcpToken(server);
    return getClient(server);
  }
  if (e instanceof Error && /returned HTTP 404/.test(e.message)) {
    logger.error({ url: server.url }, "MCP URL is wrong");
  }
  throw e;
}

Prevention

When it happens

Trigger: Initial connect to an MCP server that returns 401 Unauthorized, 403 Forbidden, 404 Not Found, 500/502/503, or another definitive 4xx/5xx on the StreamableHTTP handshake. Reachable whenever McpServerConfig.url is wrong, auth is missing/invalid, or the upstream is erroring.

Common situations: Wrong base URL for the MCP server; missing or expired bearer token in headers; server requires a different auth scheme; upstream incident returning 5xx; reverse proxy returning 502/504 because the MCP backend is down; CORS or path prefix issues surfacing as 404.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/b6270584b4428f5a. Report an issue: GitHub.