jackwener/OpenCLI · error · Error
Detached HEAD — checkout a branch first
Error message
Detached HEAD — checkout a branch first
What it means
At src/daemon.ts:496 the daemon's HTTP server (httpServer, declared from createServer) wraps every handleRequest invocation with a catch that discards the error and replies with a bare 500 and no body: res.writeHead(500); res.end(). Clients therefore see an empty message — the response carries status 500 but no error JSON explaining what failed. The declared symbol region is this server-creation site; the empty message is the deliberate (but unhelpful) swallowing of the underlying rejection.
Source
Thrown at autoresearch/engine.ts:103
/** Phase 0: Precondition checks */
private checkPreconditions(): void {
// Git repo exists
try { execStrict('git rev-parse --git-dir'); }
catch { throw new Error('Not a git repository'); }
// Clean working tree
const status = exec('git status --porcelain');
if (status) throw new Error(`Working tree not clean:\n${status}`);
// No stale locks
if (existsSync(join(ROOT, '.git', 'index.lock'))) {
throw new Error('Stale .git/index.lock found — remove it first');
}
// Not detached HEAD
try { execStrict('git symbolic-ref HEAD'); }
catch { throw new Error('Detached HEAD — checkout a branch first'); }
}
/** Phase 5: Run verify command and extract metric */
private runVerify(): number | null {
this.log(' verify...');
const output = exec(this.config.verify, { timeout: 300_000 });
return extractMetric(output);
}
/** Phase 5.5: Run guard command */
private runGuard(): boolean {
if (!this.config.guard) return true;
this.log(' guard...');
try {
execStrict(this.config.guard, { timeout: 300_000 });
return true;
} catch {
return false;View on GitHub (pinned to 49907e53dc)
Solutions
- Check daemon logs at the time of the request — the rejection is logged server-side even though the client sees an empty 500.
- Change the catch handler to send a JSON error body (e.g. jsonResponse(res, 500, { ok:false, error: message })) so failures are diagnosable from the client side.
- Reproduce the request with curl and verify the payload matches the endpoint's expected schema (valid JSON, correct fields).
- Ensure routes wrap their logic in the existing try/catch that maps DaemonCommandFailure and timeouts to proper status codes (400/408).
Example fix
// before
handleRequest(req, res).catch(() => { res.writeHead(500); res.end(); });
// after
handleRequest(req, res).catch((err) => {
jsonResponse(res, 500, { ok: false, error: err instanceof Error ? err.message : 'Internal error' });
}); Defensive patterns
Strategy: validation
Validate before calling
const body = JSON.stringify(payload);
JSON.parse(body); // fail fast client-side before hitting the daemon
const res = await fetch(url, { method: 'POST', body });
if (res.status === 500 && (await res.text()).length === 0) {
console.error('Daemon returned empty 500 — check daemon logs for the swallowed handleRequest rejection');
} Type guard
function isDaemonErrorResponse(res: Response): res is Response & { status: 400 | 408 | 500 } {
return res.status >= 400;
} Try / catch
const res = await fetch(daemonUrl, { method: 'POST', body });
if (!res.ok) {
const text = await res.text();
if (!text) throw new Error(`Daemon 500 with empty body (src/daemon.ts:496) — inspect daemon logs`);
const { error } = JSON.parse(text);
throw new Error(error);
} Prevention
- Validate request payloads against the endpoint schema before sending to the daemon.
- Watch daemon logs in development — the client-side empty 500 hides the real error there.
- Patch the .catch handler to return a JSON error body so failures are self-describing.
- Distinguish status codes: 408 = timeout, 400 = invalid request, empty 500 = swallowed internal rejection.
When it happens
Trigger: Any HTTP request to the daemon where handleRequest's promise rejects — malformed JSON body, handler bug, or thrown DaemonCommandFailure outside the handled branch — triggering the .catch(() => { res.writeHead(500); res.end(); }) fallback with an empty response body.
Common situations: A client sends an unparseable POST body to a daemon endpoint; an internal handler throws before the route-level try/catch at line 479 can respond; the request is aborted mid-handler causing a downstream rejection; a bug in a new route escapes the existing error mapping.
Related errors
- coingecko derivatives returned HTTP ${resp.status}
- Ctrip flight API returned HTTP ${status || 'unknown'}
- ${label} returned HTTP ${resp.status}
- HTTP ${result.httpStatus} from /api/auth/session
- mdn search returned HTTP ${resp.status}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/ee39f3ecaf45005a.
Report an issue: GitHub.