← All context files

lite/request-and-response.md

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.

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 object means the container was null/undefined — JScript names the property being read, not the variable that was empty. Reading undefined.createdGroupId produces 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

  1. An app try/catch around send() will swallow the halt, because the sentinel is a thrown JS value (unlike ASP's Response.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
    }
    

    isResponseEnd and RESPONSE_END are exported from the package.

  2. Deferred send() (inside setTimeout, 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.