janhq/jan · error
( )
Error message
${friendly} (${requestUrlOf(input)}) What it means
In createCustomFetch, when baseFetch itself rejects (before any HTTP response), describeTransportError converts the low-level error into a friendly message and rethrows it with the request URL appended in parentheses. If the error is not a recognized transport failure it is rethrown unchanged. This surfaces DNS failures, refused connections, TLS errors, and aborts with context about which endpoint failed.
Solutions
- Check the URL in the error message and verify the server is running and reachable (curl the base URL).
- Fix the provider base_url in Settings > Model Providers (correct host/port/scheme).
- Inspect the underlying cause: start the model server session, fix TLS/proxy settings, or retry after connectivity is restored.
Example fix
// before provider.base_url = 'http://localhost:808' // after provider.base_url = 'http://localhost:8080' // server actually listening here
Defensive patterns
Strategy: retry
Validate before calling
const url = new URL(provider.base_url)
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('base_url must be http(s)') Type guard
const isTransportError = (e) => e instanceof TypeError || ['ECONNREFUSED','ENOTFOUND','ECONNRESET','fetch failed'].some(s => String(e?.cause ?? e).includes(s))
Try / catch
try { await fetchChat(url, init) } catch (e) { if (/\(http/.test(e.message)) { await backoffRetry(() => fetchChat(url, init), 3); } else throw e } Prevention
- Health-check the base_url with a cheap GET before chat calls.
- Pin correct ports and schemes in provider settings; lint base_url values.
- Wrap all provider fetches in a retry-with-backoff helper for transient transport errors.
When it happens
Trigger: Base URL host unreachable or DNS failing; server process not listening on the port; TLS certificate errors; network offline; request aborted mid-flight when the underlying error maps to a known transport cause.
Common situations: Local inference server (llama.cpp/MLX session) not started; typo'd base_url like http://localhost:808 vs :8080; CORS/proxy interception; firewall or VPN blocking the endpoint; Docker networking misconfig.
Understand the failure class
Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.
Related errors
- Cannot connect to at . Please check that the service is…
- Failed to fetch model catalog
- All endpoints failed
- API request failed with status
- Authentication failed: API key is required or invalid for
AI-assisted analysis of janhq/jan@7205d770c1 (2026-09-17).
Data as JSON: /api/errors/e89d75d32a6aeffe.
Report an issue: GitHub.
Appendix: source
Thrown at web-app/src/lib/model-factory.ts:538
let rawBody: Record<string, unknown> | null = null
if (init?.method === 'POST' || !init?.method) {
try {
rawBody = init?.body ? JSON.parse(init.body as string) : {}
} catch (e) {
throw new Error(
`Failed to parse request body as JSON: ${e instanceof Error ? e.message : String(e)}`
)
}
init = { ...init, body: JSON.stringify(buildBody(rawBody!, true)) }
}
let res: Response
try {
res = await baseFetch(input, init)
} catch (err) {
const friendly = describeTransportError(err)
if (!friendly) throw err
throw new Error(`${friendly} (${requestUrlOf(input)})`)
}
if (res.ok) {
// OpenAI-compatible servers may interleave custom named SSE events (e.g.
// tool-progress) with chat.completion.chunk data; the AI SDK validates
// every data line against the chunk schema, so strip non-default events.
// Opt-in only: Anthropic and the OpenAI Responses API use named SSE
// events as their protocol, so filtering there blanks the whole stream.
const contentType = res.headers.get('content-type') || ''
if (
filterNamedSseEvents &&
res.body &&
contentType.includes('text/event-stream')
) {
return new Response(filterDefaultSseEvents(res.body), {
status: res.status,
statusText: res.statusText,
headers: res.headers,
})View on GitHub (pinned to 7205d770c1)