lite – req.body, send() and Halting Semantics
req.body is whatever the client sent — not array-wrapped
body.ts returns parseJson(data): the raw parsed JSON, exactly as it arrived.
- Client sends
{...}→req.bodyis that object. - Client sends
[{...}]→req.bodyis that array.
rapid.js frontends (LPA and friends): withParams(payload).post() calls
axios.post(url, payload, options) — the payload goes over as a plain object,
not wrapped in an array. So in a custom preprocess POST route:
const body = req.body; // ✅ the payload object
const body = req.body[0]; // ❌ undefined
The array convention belongs to table-based routes: lite's CRUD handler
calls isEvery(isPlainObject, body) expecting an array, and those clients
explicitly send [{...}]. That's a client-side convention, not something lite
or rapid.js applies for you.
Defensive shape, since JScript's error message points at the property rather than the container:
const body = req.body;
if (!body || typeof body !== 'object') {
send({ status: 400, message: 'A request body is required.' });
return;
}
const createdGroupId = body.createdGroupId;
JScript error signature:
'createdGroupId' is null or not an objectmeans the container was null/undefined — JScript names the property being read, not the variable that was empty. Readingundefined.createdGroupIdproduces exactly that message.
rapid.js puts params in different places per method
withParams() becomes the body for POST/PUT/PATCH but the query string
for GET/DELETE. This is why an identifier can insert correctly and then fail to
delete — see query-parsing.md, since query values are type-coerced and body
values are not.
send() halts — but this changed in 6.6.0
| Runtime | Behaviour |
|---|---|
| Classic ASP, all versions | send() → write() + end() → Response.End(), which terminates the script immediately. Not catchable by a JScript try/catch. |
| Node < 6.6.0 | send() returned normally — execution continued. This is the historical source of missing-return bugs. |
| Node ≥ 6.6.0 | send()/end() throw a sentinel (RESPONSE_END, src/responseEnd.ts) after writing, caught at lite's request boundary. Now matches ASP. |
Rule that holds in every version: treat send() as "write the response",
not "stop". Follow every conditional send() with an explicit return.
if (!user) {
send({ status: 401, message: 'Authentication failed.' });
return; // ← always
}
Two caveats on the Node sentinel
An app
try/catcharoundsend()will swallow the halt, because the sentinel is a thrown JS value (unlike ASP'sResponse.End). Any such block must re-throw it:import { isResponseEnd } from '@kyd/lite'; try { doWork(); // may call send() } catch (e) { if (isResponseEnd(e)) throw e; // preserve the halt // real error handling }isResponseEndandRESPONSE_ENDare exported from the package.Deferred
send()(insidesetTimeout, a promise callback) throws past the boundary catch and is not handled.
Escape hatch: LITE_SEND_NO_HALT=true restores the old continue-after-send
behaviour.
The double-send guard hides bugs
nodeSend checks res.headersSent; a second send() logs
[lite/send] send() called after headers already sent … and is ignored, with no
hard crash. But every statement between the two sends still ran — queries,
inserts, emails. The guard suppresses the symptom, not the cause.
param = {} defaults do not catch null
Standard JS, but it bites specifically in lite routes because the argument often comes from a nullable DB column or a JSON field:
function normalise(row = {}, legacyUser = {}) { … } // default only fires on undefined
normalise(settings, null); // → throws on first property read
'legacyAlternateEmail' is null or not an object (ASP) /
Cannot read properties of null (Node).
It usually only shows in prod, because dev data is complete — a dev username always resolves to a real record, so the null never occurs locally.
Fix: coalesce inside the function when the value can come from a nullable
row or request field. Params that can only ever be a missing argument or a
missing req.query key are genuinely undefined, and the default is fine.
function normalise(row, legacyUser) {
row = row || {};
legacyUser = legacyUser || {};
…
}
Audit by tracing call sites, not by counting = {} signatures.