Skip to content

Worked review 1: an HTTP JSON API

This is the first of four full reviews. Each one gives a complete, AI-style program, asks you to find the planted bugs using the habits from this module’s first four lessons, then shows the real tools’ real output and a fixed version that actually runs correctly. This one is a small JSON API for creating and listing “items” — a health check, a list route, and a create route, with four bugs from the AI bug catalog planted across about 60 lines.

Node.js
Output (verified on Node 24)
port looks fine relative to the minimum
health: {"ok":true}
created: {"id":1,"name":"widget"}

The demo prints three lines, then crashes before a fourth — no malformed response: line ever appears.

Real output (stderr, after the three lines above):

Error [ERR_HTTP_HEADERS_SENT]: Cannot write headers after they are sent to the client
at ServerResponse.writeHead (node:_http_server:356:11)
at sendJson (...)
at IncomingMessage.<anonymous> (...)

Four planted bugs — one boundary, one lifetime, one missing listener, one type. Try to name all four before opening the answers below.

Bug 1 — body += chunk; with no size check

Symptom: the POST /items handler accumulates the entire request body into one string with no upper bound at all.

Evidence: nothing in this demo run actually exercises a huge body, which is exactly the danger — the bug is invisible on every normal-sized request and only costs memory the day a client (or an attacker) sends gigabytes.

Fix: track the accumulated byte count and reject (413, then req.destroy()) once it passes a MAX_BODY limit, exactly as the JSON API lesson’s route does.

Bug 2 — sendJson(res, 500, ...) right after sendJson(res, 400, ...)

Symptom: on a malformed-JSON request, the catch block calls sendJson twice — and sendJson always calls res.writeHead() then res.end().

Evidence: the real crash above — ERR_HTTP_HEADERS_SENT, thrown from the second sendJson call’s res.writeHead(), since headers were already sent by the first call moments earlier.

Fix: every response-sending helper needs a guard against running twice: if (res.writableEnded) return; at the top of sendJson, and a return right after the first sendJson call in the catch block (which this file already has — the second call is simply dead code that should never have been written).

Bug 3 — no error listener anywhere

Symptom: neither the server nor any individual res has an .on('error', ...) handler.

Evidence: this is why bug 2 crashes the whole process instead of just failing one request — ERR_HTTP_HEADERS_SENT is emitted as an 'error' event on the response stream, and with zero listeners for it, Node treats it as fatal.

Fix: res.on('error', function (err) { console.error(...); }) inside the request handler, and server.on('error', function (err) { console.error(...); }) at the top level, so a single bad response degrades to a logged error instead of taking down every other in-flight request too.

Bug 4 — PORT > process.env.MIN_ALLOWED_PORT

Symptom: the printed message is backwards: '9000' > '10000' is compared as a string (both sides are strings), so it compares lexicographically — '9' > '1' is true — and the code prints “port looks fine” for a port that is actually below the configured minimum.

Evidence: the first line of real output above — port looks fine relative to the minimum — is the wrong branch; 9000 < 10000 should have printed the warning instead.

Fix: Number(process.env.PORT) > Number(process.env.MIN_ALLOWED_PORT).

Node.js
Output (verified on Node 24)
warning: port is below the configured minimum
health: {"ok":true}
created: {"id":1,"name":"widget"}
malformed response: {"error":"malformed JSON"}

All four requests now complete: the port warning is correct, the health check and the valid create both work, and the malformed request gets a clean 400 instead of crashing the process.

npx eslint (flat config with eslint-plugin-n + eslint-plugin-promise) on the buggy version: zero problems. None of the four bugs have a matching rule — a missing body limit, a double response-send, a missing error listener, and a string/string comparison are all outside what this course’s installed rules check for.

node --trace-warnings on the buggy version: no warning at all — this file crashes with an uncaught exception, not a warning, so --trace-warnings has nothing to add here (that flag is for bug 12 in the catalog, listener leaks, not this one).

Running the file is what actually caught three of the four bugs (2, 3, and 4, all visible in the real output above); bug 1 (the missing body limit) produced no visible symptom in this run at all — it takes an oversized request, not just a malformed one, to observe it, exactly like pattern 9 in the AI bug catalog.

The buggy version crashes with ERR_HTTP_HEADERS_SENT, not ERR_STREAM_WRITE_AFTER_END like the equivalent pattern in the AI bug catalog. Why the different error?
Why does bug 3 (no error listener) matter even though the file never explicitly triggers an "error" event on purpose?
What does the real output's first line, "port looks fine relative to the minimum," actually prove about bug 4?
Why did eslint report zero problems on a file with four real, planted bugs?