affaan-m/ECC · error · Error

Expected JSON response from control pane:

Error message

Expected JSON response from control pane: 

What it means

Thrown by readJsonResponse when the control-pane HTTP response body cannot be parsed as JSON. The control-pane API is contractually expected to return application/json on every route (including errors); a non-JSON body violates that contract. The wrapped error.message comes from the underlying JSON parser and usually indicates an empty body, an HTML error page, or a proxy/HTML 404 intercepting the request.

Source

Thrown at scripts/lib/control-pane/ui.js:444

      const target = $(targetSelector);
      if (!target) return;
      target.hidden = false;
      target.textContent = formatError(error);
    }

    function clearError(targetSelector) {
      const target = $(targetSelector);
      if (!target) return;
      target.hidden = true;
      target.textContent = '';
    }

    async function readJsonResponse(response) {
      let payload;
      try {
        payload = await response.json();
      } catch (error) {
        throw new Error('Expected JSON response from control pane: ' + error.message);
      }

      if (!response.ok) {
        const detail = payload && payload.error ? payload.error : response.status + ' ' + response.statusText;
        throw new Error(detail);
      }

      return payload;
    }

    function statePill(stateName) {
      const state = String(stateName || 'unknown');
      const klass = ['running', 'done'].includes(state)
        ? 'good'
        : ['failed', 'blocked'].includes(state)
          ? 'bad'
          : ['pending', 'ready'].includes(state)
            ? 'warn'

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Confirm the control-pane server is running and reachable (curl -i http://localhost:<port>/api/health) and that it returns Content-Type: application/json.
  2. Inspect the raw response body/status before parsing — log response.status, response.headers.get('content-type'), and await response.text() to see what was actually returned.
  3. Fix or register the missing route so the control-pane returns JSON for that path.
  4. If a proxy sits in front, configure it to pass through /api/* untouched and serve its own errors as JSON.

Example fix

// before
const payload = await response.json();

// after — fail with the real body when it isn't JSON
const text = await response.text();
let payload;
try { payload = JSON.parse(text); }
catch (e) { throw new Error(`Expected JSON from ${response.url} (status ${response.status}, ct ${response.headers.get('content-type')}): ${text.slice(0, 200)}`); }
Defensive patterns

Strategy: try-catch

Validate before calling

const ct = response.headers.get('content-type') || '';
if (!ct.includes('application/json')) {
  const body = await response.text();
  throw new Error(`Non-JSON response (ct=${ct}, status=${response.status}): ${body.slice(0,200)}`);
}

Type guard

function isJsonResponse(response) {
  const ct = response.headers.get('content-type') || '';
  return ct.includes('application/json');
}

Try / catch

try {
  await readJsonResponse(response);
} catch (e) {
  if (/Expected JSON response/.test(e.message)) {
    // server returned non-JSON; surface status/content-type for diagnosis
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling any control-pane endpoint (e.g. POST /api/work-items/<id>/claim via postWorkItem) whose response body is empty or HTML; a reverse proxy returning an HTML 502/404; the local server crashed mid-response; a route is missing so the static-file server returns index.html.

Common situations: The control-pane server is not running or crashed (connection returns HTML); a request was sent before the server finished booting; the URL path was wrong and a catch-all served HTML; a corporate proxy injected an HTML block page; CORS/preflight returned HTML.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/701231a2399c3a20. Report an issue: GitHub.