slopus/happy · warning
timeout
Error message
timeout
What it means
The hook server's /hook/session-start handler enforces a 5-second timeout: if the request body isn't fully received/processed in time, it responds 408 with body 'timeout'. This guards against Claude never closing stdin.
Source
Thrown at packages/happy-cli/src/claude/utils/startHookServer.ts:105
/**
* Start a dedicated HTTP server for receiving Claude session hooks
*
* @param options - Server options including the session hook callback
* @returns Promise resolving to the server instance with port info
*/
export async function startHookServer(options: HookServerOptions): Promise<HookServer> {
const { onSessionHook } = options;
return new Promise((resolve, reject) => {
const server: Server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
// Only handle POST to /hook/session-start
if (req.method === 'POST' && req.url === '/hook/session-start') {
// Set timeout to prevent hanging if Claude doesn't close stdin
const timeout = setTimeout(() => {
if (!res.headersSent) {
logger.debug('[hookServer] Request timeout');
res.writeHead(408).end('timeout');
}
}, 5000);
try {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(chunk as Buffer);
}
clearTimeout(timeout);
const body = Buffer.concat(chunks).toString('utf-8');
logger.debug('[hookServer] Received session hook:', body);
let data: SessionHookData = {};
try {
data = JSON.parse(body);
} catch (parseError) {
logger.debug('[hookServer] Failed to parse hook data as JSON:', parseError);View on GitHub (pinned to b824cd0a46)
Solutions
- Ensure the hook client closes stdin after writing the session-start payload
- Send the payload in one write and end the request immediately
- Increase or remove the 5s timeout if payloads are legitimately large
- Check hook client logs for why the request never completed
Example fix
// before
res.writeHead(408).end('timeout');
// after
req.destroy();
res.writeHead(408).end('timeout'); Defensive patterns
Strategy: retry
Validate before calling
const payload = JSON.stringify(sessionData);
const res = await fetch(url, { method: 'POST', body: payload, signal: AbortSignal.timeout(4000) });
await res.body?.cancel(); // ensure stream completes Try / catch
try {
const res = await fetch(hookUrl, { method: 'POST', body: payload });
if (res.status === 408) {
// retry once with the payload written and stream closed promptly
}
} catch (e) { /* handle */ } Prevention
- Always close stdin/end the request body immediately after writing the payload
- Keep session-start payloads small and write them in one chunk
- Treat a 408 from the hook server as a client-side hang and fix the sender
- Avoid retry loops that hold the request open longer than 5s
When it happens
Trigger: POST to /hook/session-start where the client doesn't close the request stream (stdin not closed) or body reading takes longer than 5 seconds; the timeout fires and writes the 408 response.
Common situations: Hook integrations that keep the request/stream open; large session payloads over a slow pipe; a hung Claude process that never signals end-of-input.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/7781ca7589f8e689.
Report an issue: GitHub.