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

  1. Check the URL in the error message and verify the server is running and reachable (curl the base URL).
  2. Fix the provider base_url in Settings > Model Providers (correct host/port/scheme).
  3. 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

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


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)