Classic ASP / JScript – Runtime Gotchas
Beyond missing ES6 built-ins (see es6-builtins.md), the JScript/ASP runtime
behaves differently from Node in ways Babel can't paper over. These are the
recurring ones in KYD apps (lite v6 framework: SPM, EMP, LPA, CFT).
'Reflect' is undefined — a class extending a native (usually Error)
Symptom: the bundle throws 'Reflect' is undefined at runtime in ASP.
Cause: Babel compiles class X extends Error (or any native built-in) with
_wrapNativeSuper(Error) → _isNativeReflectConstruct() → Reflect.construct.
JScript has no Reflect, so it crashes the moment that path executes.
Most common trigger: two route files registered for the same path (e.g. a
GET file and a POST file). The wrong-method request reaches validatedMethod,
which throws new ClientError(msg, 405) — and ClientError extends Error is
exactly the construct that needs Reflect.
Fix: never split methods for one path across two files. Use a single route
with methods: ['GET', 'POST'] and branch inside preprocess on req.method.
Note the route pipeline still calls validatedMethod even when you guard in
preprocess, so the single-file form is mandatory, not just tidier.
Response.End() / send() halts execution — but only in ASP
In ASP, Response.End() terminates the script immediately. The lite
send() / end() helpers call it, so in an ASP build everything after a
send() is dead code. A JScript try/catch does NOT catch Response.End.
In Node, send() does not halt — execution continues past it. So a
conditional send() that you expect to short-circuit must be followed by an
explicit return, or the Node and ASP builds diverge:
if (!user) {
send(res, 401, { error: 'Unauthorized' })
return // ← required, or Node keeps running the handler
}
IIS routing — no URL Rewrite, no PATH_INFO
IIS Classic ASP rejects index.asp/path/info URLs by default (allowPathInfo
is off on the ASP handler), and dev servers often have no URL Rewrite module.
- The frontend proxy (
base.js) must always requestindex.aspdirectly and communicate the route via theOriginal-Url-Overrideheader. @kyd/utilssetsOriginal-Url-Overrideinternally; hand-rolled / Rapid.js proxies must add it manually, plusurl: process.env.VUE_APP_API_ADDRESSin the axios interceptor to strip the path from the actual request URL.- lite reads
Original-Url-Override→HTTP_ORIGINAL_URL_OVERRIDEfirst ingetRoute(); this is the reliable fallback when PATH_INFO / URL Rewrite aren't available. - Don't deploy an app-level
web.configin dev if the root IISweb.configalready setsAccess-Control-Allow-Headers: *— you'll get duplicate CORS headers.
Route ordering & required fields
- Fixed paths before parameterised siblings: register
/demand/archivebefore/demand/:id.preprocessfires on path match regardless of themethodsarray, so a greedy:idroute can swallow the fixed path. tableis required for CRUD routes, but custompreprocess()-only routes can omit it entirely (lite ≥ 6.2.65).- Give reporting / custom routes an explicit
path:— the kebab-case fallback is unreliable in ASP builds.
Query parsing (identical across Node dev, ASP dev, ASP prod)
The compiled qs + reviveQuery code is the same everywhere, so these hold in
all three environments:
reviveQueryconverts all-digit strings to numbers (stripping leading zeros). To preserve a zero-padded value, sendfilter[key][parse]=falsefrom the frontend or coerce withString()in the route.- lite v6
select()requiresfilteras an array ([{ key, equals }]); the v1 frontend sends an object (filter[key][value]=x). Convert with aconvertLegacyFilter()helper. Symptom if you don't:"filter expected type: 'Array' but received type: 'Object'". descending,page/rowsPerPageare converted automatically;searchis not a built-inselect()param and must be handled manually.
Fetch-all + slice-in-JS times out in ASP (but flies in Node)
Symptom: a list endpoint works instantly under npm run dev (Node) but
times out (~60s) once deployed as Classic ASP.
Cause: a query that pulls the whole table/view and paginates in JavaScript —
select(db, view, { itemsPerPage: 10000, paginate: false }) then .slice() /
.filter() / .length in JS. In Node the dev DB is tiny and the rows marshal
for free. In ASP, lite reads results through an ADO recordset, and every
row and every field is a COM round-trip — so thousands of rows × N columns is
tens of thousands of COM calls. Large prod views (e.g. a CFT_Users view over
all Defence accounts) blow the request timeout. This is a data-volume cliff: it
won't show in dev and gets worse as prod data grows.
Fix: let SQL do the paging and counting. select() with page +
itemsPerPage and paginate: true (the default) emits a ROW_NUMBER() window
that returns only the requested page, plus a separate getCount() for
meta.totalItems. Only one page of rows is ever marshalled:
// ❌ times out in ASP on a large view
const all = select(db, 'CFT_Users', { columns, filter, itemsPerPage: 10000, paginate: false })
const data = all.data.filter(r => r.id && r.name).map(normalize).slice(offset, offset + perPage)
// ✅ SQL does the paging + count
const result = select(db, 'CFT_Users', {
columns, filter, sort,
page, itemsPerPage, // paginate defaults to true
})
const data = result.data.map(normalize)
const totalItems = result.meta.totalItems
Push any JS-side row exclusions into the SQL filter (e.g. { key: 'name', isNotNull: true }) so the COUNT and the page agree. Bounded lookups
(one user's roles, reference data of a few dozen rows) are fine to fetch whole —
this only bites unbounded result sets. For reporting endpoints that genuinely
need every row to aggregate, push the aggregation into SQL with group (GROUP BY)
rather than summing thousands of marshalled rows in JS.
DB connection
The DB connection comes from Application.contents('SQLConn') (set in
global.asa / IIS Application state), not from a database param passed to
select(). Routes that target a non-default DB pass it explicitly, e.g.
database: process.env.CFT_LEGACY_DB_NAME.