fix: alle DELETE-verzoeken faalden op een lege JSON-body (v0.4.02-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
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)
This commit is contained in:
parent
4517f592f7
commit
45f66044f2
5 changed files with 77 additions and 3 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.4.01-beta
|
0.4.02-beta
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
/* 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. */
|
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(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -675,7 +675,11 @@ try{ localStorage.removeItem("teach.token"); }catch(e){}
|
||||||
let currentUser = null; /* {id, username, displayName, role, schoolId, classId} */
|
let currentUser = null; /* {id, username, displayName, role, schoolId, classId} */
|
||||||
|
|
||||||
async function api(path, opts={}){
|
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, {
|
const r = await fetch("/api" + path, {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
method: opts.method || (opts.body ? "POST" : "GET"),
|
method: opts.method || (opts.body ? "POST" : "GET"),
|
||||||
|
|
|
||||||
18
src/body-parser.js
Normal file
18
src/body-parser.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import api, { bootstrapSuper } from './api.js';
|
||||||
import imageApi from './images.js';
|
import imageApi from './images.js';
|
||||||
import { registerFrontend, resolveAppVersion } from './frontend.js';
|
import { registerFrontend, resolveAppVersion } from './frontend.js';
|
||||||
import { runMigrations } from './migrate.js';
|
import { runMigrations } from './migrate.js';
|
||||||
|
import { registerEmptyJsonBodyParser } from './body-parser.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
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
|
// Maak de pool bereikbaar in routes via app.pg
|
||||||
app.decorate('pg', pool);
|
app.decorate('pg', pool);
|
||||||
|
|
||||||
|
registerEmptyJsonBodyParser(app);
|
||||||
|
|
||||||
// --- Health checks (gebruikt door Docker + load balancer) --------------------
|
// --- Health checks (gebruikt door Docker + load balancer) --------------------
|
||||||
app.get('/healthz', async () => ({ status: 'ok' }));
|
app.get('/healthz', async () => ({ status: 'ok' }));
|
||||||
|
|
||||||
|
|
|
||||||
49
test/body-parser.test.js
Normal file
49
test/body-parser.test.js
Normal file
|
|
@ -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();
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue