From 45f66044f229f6563d861e32b1e8cd02441a03b2 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sat, 18 Jul 2026 03:44:39 +0200 Subject: [PATCH] fix: alle DELETE-verzoeken faalden op een lege JSON-body (v0.4.02-beta) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit De echte, uiteindelijke oorzaak achter elke "serverfout" die niet door een route-eigen foutafhandeling kwam (klas verwijderen, en zeer waarschijnlijk ook de oorspronkelijke gebruiker-verwijderen-bug): de client stuurde bij élk verzoek altijd "Content-Type: application/json" mee, ook bij een DELETE zonder body. Fastify's standaard JSON-parser weigert zo'n leeg lichaam met "Body cannot be empty when content-type is set to 'application/json'" - vóórdat er ook maar één hook of route-handler heeft kunnen draaien, dus nog vóór req.user gezet wordt en vóór enige try/catch in een route. Dat verklaart waarom eerdere per-route foutafhandeling dit nooit kon vangen: de fout zat vóór elke route. - public/js/core.js: api() stuurt Content-Type alleen nog mee als er werkelijk een JSON-body is - src/body-parser.js (nieuw): serverside vangnet dat een leeg lichaam bij Content-Type: application/json gewoon toestaat i.p.v. te weigeren - ongeacht wat een client meestuurt - test/body-parser.test.js: reproduceert de exacte foutmelding tegen een echte Fastify-requestcyclus (de gemockte-pool-tests konden dit principieel nooit vangen, vandaar dat dit alle eerdere testrondes overleefde) --- VERSION | 2 +- public/js/core.js | 8 +++++-- src/body-parser.js | 18 +++++++++++++++ src/server.js | 3 +++ test/body-parser.test.js | 49 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/body-parser.js create mode 100644 test/body-parser.test.js diff --git a/VERSION b/VERSION index 1d1b65b..e76466f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.01-beta +0.4.02-beta diff --git a/public/js/core.js b/public/js/core.js index 1ae1205..9947350 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.4.01-beta"; +const VERSION = "0.4.02-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -675,7 +675,11 @@ try{ localStorage.removeItem("teach.token"); }catch(e){} let currentUser = null; /* {id, username, displayName, role, schoolId, classId} */ async function api(path, opts={}){ - const headers = { "Content-Type": "application/json", ...(opts.headers||{}) }; + /* alleen Content-Type meesturen als er ook echt een JSON-body is (bv. niet + bij DELETE) - anders weigert Fastify's parser het lege lichaam met een + ondoorzichtige "Body cannot be empty when content-type is set to + 'application/json'", vóórdat een route-handler zelfs maar draait */ + const headers = { ...(opts.body ? { "Content-Type": "application/json" } : {}), ...(opts.headers||{}) }; const r = await fetch("/api" + path, { credentials: "same-origin", method: opts.method || (opts.body ? "POST" : "GET"), diff --git a/src/body-parser.js b/src/body-parser.js new file mode 100644 index 0000000..3b07161 --- /dev/null +++ b/src/body-parser.js @@ -0,0 +1,18 @@ +// Sta een leeg lichaam toe bij Content-Type: application/json (bv. DELETE- +// verzoeken zonder body, zoals de client die stuurt). Fastify's +// standaardparser weigert dit anders met "Body cannot be empty when +// content-type is set to 'application/json'" - vóórdat er ook maar één hook +// of route-handler heeft kunnen draaien, dus zonder gebruiker en zonder +// route-eigen foutafhandeling: elke poging eindigde in een onherleidbare +// kale "serverfout". +export function registerEmptyJsonBodyParser(app) { + app.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => { + if (!body) return done(null, undefined); + try { + done(null, JSON.parse(body)); + } catch (err) { + err.statusCode = 400; + done(err); + } + }); +} diff --git a/src/server.js b/src/server.js index 5eda97f..b5e3478 100644 --- a/src/server.js +++ b/src/server.js @@ -11,6 +11,7 @@ import api, { bootstrapSuper } from './api.js'; import imageApi from './images.js'; import { registerFrontend, resolveAppVersion } from './frontend.js'; import { runMigrations } from './migrate.js'; +import { registerEmptyJsonBodyParser } from './body-parser.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -38,6 +39,8 @@ const pool = new pg.Pool({ // Maak de pool bereikbaar in routes via app.pg app.decorate('pg', pool); +registerEmptyJsonBodyParser(app); + // --- Health checks (gebruikt door Docker + load balancer) -------------------- app.get('/healthz', async () => ({ status: 'ok' })); diff --git a/test/body-parser.test.js b/test/body-parser.test.js new file mode 100644 index 0000000..96450f2 --- /dev/null +++ b/test/body-parser.test.js @@ -0,0 +1,49 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import Fastify from 'fastify'; +import { registerEmptyJsonBodyParser } from '../src/body-parser.js'; + +// Dit is precies wat public/js/core.js's api() vóór de fix altijd deed voor +// een DELETE zonder body: Content-Type: application/json meesturen zonder +// werkelijke inhoud. Fastify's ingebouwde JSON-parser weigert dat met "Body +// cannot be empty when content-type is set to 'application/json'" - vóórdat +// er ook maar één hook of route-handler heeft kunnen draaien. Geen enkele +// gemockte-pool-test kon dit vangen (die simuleren geen echte HTTP-requests), +// vandaar deze losse test tegen een echte Fastify-request-cyclus. +test('leeg lichaam met Content-Type: application/json wordt niet geweigerd', async () => { + const app = Fastify(); + registerEmptyJsonBodyParser(app); + app.delete('/thing/:id', async () => ({ ok: true })); + await app.ready(); + const res = await app.inject({ + method: 'DELETE', url: '/thing/1', headers: { 'content-type': 'application/json' }, + }); + assert.equal(res.statusCode, 200, res.body); + assert.deepEqual(res.json(), { ok: true }); + await app.close(); +}); + +test('ongeldige JSON in het lichaam geeft nog steeds een nette 400', async () => { + const app = Fastify(); + registerEmptyJsonBodyParser(app); + app.post('/thing', async (req) => ({ received: req.body })); + await app.ready(); + const res = await app.inject({ + method: 'POST', url: '/thing', headers: { 'content-type': 'application/json' }, payload: '{niet geldig', + }); + assert.equal(res.statusCode, 400); + await app.close(); +}); + +test('een echte JSON-body werkt gewoon zoals voorheen', async () => { + const app = Fastify(); + registerEmptyJsonBodyParser(app); + app.post('/thing', async (req) => ({ received: req.body })); + await app.ready(); + const res = await app.inject({ + method: 'POST', url: '/thing', headers: { 'content-type': 'application/json' }, payload: { a: 1 }, + }); + assert.equal(res.statusCode, 200, res.body); + assert.deepEqual(res.json(), { received: { a: 1 } }); + await app.close(); +});