json1-2 Exercise 2: Reading and Sending JSON Over HTTP ===================================================== Run with Node 24.18.0: node ex2.mjs (the servers use a random local port; it does not appear in this output) --- ex2.mjs --- // Exercise 2: reading JSON over HTTP safely import http from 'node:http'; const routes = { '/ok': [200, 'application/json', '{"id":7,"name":"Ada"}'], '/notfound': [404, 'application/json', '{"error":"no such user"}'], '/html': [502, 'text/html', 'Bad Gateway'], '/empty': [204, 'application/json', ''], '/badjson': [200, 'application/json', '{"id":7,'], '/wrongtype': [200, 'text/plain', '{"id":8}'], }; const server = http.createServer((req, res) => { const [s, t, b] = routes[req.url]; res.writeHead(s, { 'content-type': t }); res.end(b); }); await new Promise(r => server.listen(0, '127.0.0.1', r)); const base = 'http://127.0.0.1:' + server.address().port; async function naive(url) { return (await fetch(url)).json(); } async function getJson(url) { const res = await fetch(url); const type = res.headers.get('content-type') ?? ''; const body = await res.text(); // read once, as text if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + (type.includes('json') ? body : body.slice(0, 40))); if (res.status === 204 || body === '') return null; try { return JSON.parse(body); } catch (e) { throw new Error('bad JSON (' + e.message + '), starts with ' + JSON.stringify(body.slice(0, 12))); } } const show = async (fn, path) => { try { return JSON.stringify(await fn(base + path)); } catch (e) { return 'THREW ' + e.message; } }; console.log('path'.padEnd(11) + 'naive: (await fetch(u)).json()'.padEnd(95) + 'getJson(u)'); for (const p of Object.keys(routes)) console.log(p.padEnd(11) + (await show(naive, p)).padEnd(95) + await show(getJson, p)); const r1 = await fetch(base + '/ok'); await r1.json(); try { await r1.json(); } catch (e) { console.log('\nreading the same body twice: ' + e.constructor.name + ': ' + e.message); } // sending JSON: Content-Type matters const echo = http.createServer((req, res) => { let b = ''; req.on('data', c => b += c); req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ type: req.headers['content-type'], body: b })); }); }); await new Promise(r => echo.listen(0, '127.0.0.1', r)); const u = 'http://127.0.0.1:' + echo.address().port; const send = async init => JSON.stringify(await (await fetch(u, { method: 'POST', ...init })).json()); console.log('\nPOST body JSON.stringify, no header :', await send({ body: JSON.stringify({ a: 1 }) })); console.log('POST body JSON.stringify, header :', await send({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ a: 1 }) })); console.log('POST body an object (no stringify) :', await send({ headers: { 'Content-Type': 'application/json' }, body: { a: 1 } })); server.close(); echo.close(); Output: path naive: (await fetch(u)).json() getJson(u) /ok {"id":7,"name":"Ada"} {"id":7,"name":"Ada"} /notfound {"error":"no such user"} THREW HTTP 404: {"error":"no such user"} /html THREW Unexpected token '<', "Bad Gateway /empty THREW Unexpected end of JSON input null /badjson THREW Expected double-quoted property name in JSON at position 8 (line 1 column 9) THREW bad JSON (Expected double-quoted property name in JSON at position 8 (line 1 column 9)), starts with "{\"id\":7," /wrongtype {"id":8} {"id":8} reading the same body twice: TypeError: Body is unusable: Body has already been read POST body JSON.stringify, no header : {"type":"text/plain;charset=UTF-8","body":"{\"a\":1}"} POST body JSON.stringify, header : {"type":"application/json","body":"{\"a\":1}"} POST body an object (no stringify) : {"type":"application/json","body":"[object Object]"} WHY THIS WORKS AS AN ANSWER --------------------------- The server is started inside the script, so every route's behaviour is known exactly and nothing depends on a remote service. Side by side, the shortcut and the helper differ on four of the six routes: the 404 with a JSON body looks like success to the shortcut (fetch does not reject on a 404); the 502 HTML page gives the shortcut a bare "Unexpected token '<'" while the helper reports the status and the start of the body; the 204 makes the shortcut throw "Unexpected end of JSON input" where the helper returns null; and the cut-off JSON gives the helper's message the first characters of the body. The text/plain route shows that .json() ignores Content-Type. The echo route shows the sending side: without the header the server sees text/plain;charset=UTF-8, and passing an object without JSON.stringify sends the text "[object Object]". The helper is one reasonable design, not the only one: whether a 204 should be null, and how much of an HTML body to quote, are choices for your own code. Not tested: timeouts, redirects, compressed bodies, and servers that send a charset other than UTF-8.