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
- For 401/403: add or refresh McpServerConfig.headers Authorization.
- For 404: verify the MCP server URL and path (often /mcp or /sse).
- For 5xx: confirm upstream health, then wait out the 5s cooldown (or evictFromPool) before retrying.
- 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
- Validate McpServerConfig.url shape and auth header presence at config-load time.
- Inspect err.cause for the SDK's original transport error to distinguish auth vs network vs cert.
- Surface the embedded HTTP status to monitoring so 4xx config drift is visible.
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
- MCP server "${server.name}" is in cooldown (HTTP ${cd.status
- HTTP transport failed: ${String(firstError instanceof Error
- Failed to fetch ${baseURL}/models: ${response.status} ${resp
- User token not found
- Unauthorized
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/b6270584b4428f5a.
Report an issue: GitHub.