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

  1. Ensure the hook client closes stdin after writing the session-start payload
  2. Send the payload in one write and end the request immediately
  3. Increase or remove the 5s timeout if payloads are legitimately large
  4. 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

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.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/7781ca7589f8e689. Report an issue: GitHub.