star7th/showdoc · error · Error

HTTP ${res.status}: ${res.statusText}

Error message

HTTP ${res.status}: ${res.statusText}

What it means

Thrown by the frontend SSE client in sendAgentMessage() after POSTing JSON to {serverHost}/api/agent/agent: the fetch resolved but res.ok was false, i.e. the server answered with a 4xx/5xx status, so the code discards the response and throws `HTTP <status>: <statusText>`. Note that over HTTP/2 statusText is always empty, so the real message often looks like `HTTP 401: ` or `HTTP 500: `. The SSE stream is never opened; params.onError receives this Error.

Source

Thrown at web_src/src/api/aiAgent.ts:271

  const url = serverHost + '/api/agent/agent'

  // 异步执行,不 await
  ;(async () => {
    let reader: ReadableStreamDefaultReader<any> | null = null
    try {
      const res = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Accept: 'text/event-stream',
          'Cache-Control': 'no-cache',
        },
        body: JSON.stringify(body),
        signal: controller.signal,
      })

      if (!res.ok) {
        throw new Error(`HTTP ${res.status}: ${res.statusText}`)
      }

      if (!res.body) {
        throw new Error('Response body is empty, server did not return a stream. Please try again later.')
      }

      reader = res.body.getReader()
      const decoder = new TextDecoder('utf-8')
      let buffer = ''

      while (true) {
        const { value, done } = await reader.read()
        if (done) break

        buffer += decoder.decode(value, { stream: true })

        // SSE 按双换行分割
        const parts = buffer.split('\n\n')

View on GitHub (pinned to 6a3fa91eee)

Solutions

  1. Log/inspect res.status and the response body (await res.text() before throwing) to identify the exact status; 401/403 means token, 404 means wrong server host/route, 500 means server-side logs (check PHP error log / bootstrap failure), 502/504 means proxy timeout.
  2. For 401/403: refresh user_token (re-login) or ensure getGuestToken() returns a valid guest token before calling sendAgentMessage.
  3. For 404: verify getServerHost() resolves to the server that actually exposes /api/agent/agent and that the server build is current.
  4. For 500: fix the server-side cause (see server logs; commonly the bootstrap DB failure in server/app/Common/bootstrap.php).
  5. For 502/504 on long turns: raise proxy_read_timeout and disable buffering for the /api/agent/ location (proxy_buffering off; X-Accel-Buffering: no already sent by the app).
  6. Include the status text and a short body excerpt in the thrown Error so users and logs see the real cause.

Example fix

// before
if (!res.ok) {
  throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}

// after
if (!res.ok) {
  const detail = await res.text().catch(() => '')
  const err = new Error(`HTTP ${res.status}: ${res.statusText || ''} ${detail.slice(0, 200)}`.trim())
  ;(err as any).status = res.status
  throw err
}
Defensive patterns

Strategy: try-catch

Validate before calling

const userToken = getUserToken()
if (!userToken && !getGuestToken()) {
  params.onError(new Error('Not authenticated: no user_token or guest_token available'))
  return { abort: () => {} }
}

Type guard

function isHttpStatusError(e: unknown): e is Error & { status: number } {
  return e instanceof Error && /^HTTP \d{3}/.test(e.message) && typeof (e as any).status === 'number'
}

Try / catch

// inside the existing async IIFE in sendAgentMessage, keep ONE catch that classifies:
try {
  // ... fetch + SSE loop ...
} catch (e: any) {
  if (e.name === 'AbortError') return            // user-initiated abort, not an error
  if (typeof e.status === 'number') {
    if (e.status === 401 || e.status === 403) params.onError(new Error('Login expired, please re-authenticate'))
    else if (e.status >= 500) params.onError(new Error('Server error, please retry later'))
    else params.onError(e)
  } else {
    params.onError(e instanceof Error ? e : new Error(String(e)))
  }
}

Prevention

When it happens

Trigger: POST /api/agent/agent with an expired/invalid user_token or missing guest_token (server rejects auth -> 401/403); getServerHost() pointing at the wrong origin or an old deployment so the route 404s; a PHP fatal on the server (including the RuntimeException from server/app/Common/bootstrap.php when the SQLite/MySQL connection fails) surfacing as 500; a nginx/apache proxy in front of the PHP server timing out a long agent turn -> 502/504; request body rejected (413) when editor_content is very large.

Common situations: User left the tab open past token expiry and sends a new agent message; frontend deployed against a server that was rolled back or missing the agent routes; SQLite file permissions broke after a migration, making every server route return 500; reverse proxy (nginx) buffering/timeout defaults killing long-lived SSE POSTs; HTTP/2 deployments where statusText is empty making the error look malformed.

Related errors


AI-assisted analysis of star7th/showdoc@6a3fa91eee (2026-08-21). Data as JSON: /api/errors/c2c9c52b53cd6a1a. Report an issue: GitHub.