BoxLang Express Development Update 8/26
BoxExpress 0.1.15→0.2.1: five patch releases fixed empty-value crashes (query/cookie/multipart), hardened proxy-IP trust for real platforms, fixed session data loss against non-memory stores, added response outcome accessors and opt-out session writes, and closed a trailing-slash bug hitting nearly every request. Then 0.2.0 replaced the JDK's HttpServer with Undertow entirely (breaking), verified at full parity and load-tested. 0.2.1 added live HTTP-layer metrics via getConnectorStatistics().
BoxExpress: 0.1.15 → 0.2.1
A run of eight releases: five patch releases hardening request parsing, sessions, and observability, then a two-release arc replacing the server transport outright (JDK → Undertow) as a breaking change, closing with a small metrics addition.
At a glance
| Version | Headline |
|---|---|
| 0.1.15 | Fixed a class of 500 crashes on ordinary empty-value input (?q=, empty cookies, empty multipart fields) |
| 0.1.16 | Trusted-proxy IP resolution hardened for real multi-hop platforms (DigitalOcean + Cloudflare) |
| 0.1.17 | Fixed session data loss against any real (non-memory) session store |
| 0.1.18 | Added response outcome accessors for logging middleware |
| 0.1.19 | Added opt-out session store writes (resave/saveUninitialized) |
| 0.1.20 | Fixed a trailing-slash bug reachable through nearly every request |
| 0.2.0 | Breaking: server transport switched from the JDK's HttpServer to Undertow |
| 0.2.1 | Added live HTTP-layer metrics via app.getConnectorStatistics() |
0.1.15 — empty-value crashes
Fix: req.query/req.cookies threw a 500 on completely ordinary
input — a query param or cookie with an empty key or value (?q=,
?=cats, Cookie: name=, Cookie: =value). Request.bx sliced strings
with left()/right() using a count that could legitimately be 0,
which BoxLang throws on ("Count cannot be zero") — the same bug class
BodyParsers.bx already guarded against, just never applied here. Fixing
it surfaced a second issue: a failed Request/Response construction was
caught silently in HttpBridge.bx — no server log line, and the real
error hidden from the response even with app.set("env", "development")
on. It now logs and honors env the same way every other error path does.
Fix: a Range header of exactly bytes= (valid, just an empty spec)
also threw a 500 — same zero-count bug in RangeParser.bx. Now falls
back to a full 200, like any other unparseable Range header.
Fix: a sweep for the same bug turned up six more, all in
Multipart.bx — most reachably, a file input submitted with nothing
selected (filename="") crashed the whole upload. Also fixed: an empty
boundary=, a part with no headers or an empty body, and a part header
with an empty name or value.
Fix: a bare : path segment (app.get("/:", ...), a typo for
:name) threw the same zero-count error at route registration — now a
proper BoxExpress.InvalidRoutePath message instead of a raw engine
error.
Fix: listen()'s TCP accept backlog defaulted to 0 (the JDK
default), a real capacity ceiling — a live load test (ab) showed
connections getting reset once concurrency passed ~65–70, even with
request handling itself staying fast. Default raised to 1024, which
pushed the same test past 150 with no other change.
0.1.16 — real-world proxy trust
Added app.set("trust proxy header", "DO-Connecting-IP") (or an ordered
array of candidates) for platforms where X-Forwarded-For isn't safe to
trust even with trust proxy on. Confirmed directly against DigitalOcean
App Platform (with Cloudflare in front) that both hops append to
X-Forwarded-For rather than replacing it — a client can prepend a
forged entry that survives to the app, making req.ip attacker-
controlled rather than just wrong. Platforms like this typically also
inject their own edge-set header (DO-Connecting-IP, CF-Connecting-IP,
Fastly-Client-IP) carrying the real client IP, checked before
X-Forwarded-For and independent of the trust proxy boolean.
Also switched AnsiColor.bx's NO_COLOR check to the native
getSystemSetting() BIF instead of Java interop — no behavior change,
verified live.
0.1.17 — session data loss against real stores
Fix: boxExpressSession() persisted req.session to the store
before next() ran, not after — so anything the request chain mutated
on it (a CSRF token, a login handler setting the user) was written to
local memory only and never reached the store. Invisible against the
default in-memory MemoryStore (structs are pass-by-reference, so the
early store.set() was really just inserting a shared reference — later
mutation stayed visible through it). A real out-of-process store
(boxExpressCacheStore()-backed JDBCStore, Redis, etc.) has to
serialize on write, breaking that accidental sharing and exposing the
bug: a CSRF token minted mid-request never got saved, so the very next
request — even the form submission that followed the page that minted
it — read back a session with no token and 403'd. Now persists after
next(), in try/finally so a throw partway through still saves
whatever req.session held; req.destroySession() skips that save so a
destroyed session isn't silently resurrected.
0.1.18 — response outcome accessors
Added Response.getStatusCode()/getBytesWritten() — expose the
eventual status code and body byte count from outside the class, for
request-logging middleware (registered first via app.use()) that
otherwise can't observe a request's outcome. Both reflect the real
outcome even when a route never called status() directly, since every
non-200 terminal method (redirect(), sendStatus(),
sendFileRange()'s 206/304/416) routes through it internally.
0.1.19 — opt-out session store writes
Added resave/saveUninitialized options to boxExpressSession(),
mirroring express-session's own options of the same name. By default the
middleware writes to the store on every request, even one that never
touches req.session — free against the in-memory store, a real cost
against anything out-of-process. saveUninitialized: false skips both
the store write and the Set-Cookie for a new, untouched session;
resave: false skips the store write (cookie still refreshed) for an
existing, untouched session. A session that is modified is always
saved regardless. Both default to true (historical behavior), so
nothing changes for existing consumers unless they opt in.
Added Response.onBeforeSend(callback) to make saveUninitialized
possible — a callback that runs once, synchronously, the instant before
headers are actually flushed. Needed since whether to send a session
cookie can depend on what next() did to req.session, but by the time
next() returns, a terminal handler downstream has normally already
flushed headers. Same technique express-session gets via monkey-patching
res.end, done explicitly here since BoxLang can't intercept that way.
0.1.20 — a trailing slash bug reachable by nearly every request
Fix: Router.bx reconstructed req.path for app.use()-mounted
middleware/sub-routers from segments produced by splitting on /, which
drops empty segments — so a trailing slash on the incoming path left no
trace once rejoined. Since nearly every request passes through at least
one app.use() layer (even one mounted at root /), this silently
stripped trailing slashes from req.path everywhere downstream, breaking
anything that distinguishes /foo/ from /foo — notably
boxExpressStatic()'s directory-index resolution (/foo/ →
/foo/index.html). Now reattached from the pre-mutation path string.
Static file serving now also mirrors express.static(): a directory
request without the trailing slash redirects (301) to the
slash-suffixed URL instead of 404ing, since relative asset links in the
served HTML only resolve correctly against it. Built from
req.originalUrl rather than req.path, since originalUrl is captured
once at construction and unaffected by the mount-prefix stripping above.
0.2.0 — Undertow replaces the JDK's HttpServer (Breaking)
BoxExpress's server transport is now Undertow
exclusively, replacing the JDK-bundled com.sun.net.httpserver.HttpServer
it ran on before.
Verified at full behavioral parity against the previous JDK-backed server before the switch — the entire test suite passed running for real against either engine — and load-tested with no meaningful throughput/latency difference on the routes tested.
The pluggable HttpServerAdapter seam this module briefly carried while
both engines existed (an injectable adapter via
new BoxExpress(customAdapter), plus a server.engine setting) has since
been removed now that Undertow is the only engine — listen()/close()
in BoxExpress.bx talk to Undertow directly. models/adapters/ still
holds the Undertow-specific wiring classes (exchange wrapping, handler
glue, the virtual-thread dispatcher), just not as a swappable interface.
Along the way, each request is dispatched onto its own virtual thread
(matching the JDK server's Executors.newVirtualThreadPerTaskExecutor()
model), rather than falling back to Undertow's default bounded XNIO
worker pool — a real regression caught and fixed before release, not
left as a silent behavior change.
0.2.1 — live HTTP-layer metrics
Added app.getConnectorStatistics() — live HTTP-layer metrics straight
from Undertow's own listener: active connections/requests, total request
count, bytes sent/received, error count, processing time. Complements
JVM/OS-level monitoring (java.lang.management's memory/CPU/GC MXBeans)
with numbers those can't see, for something like a live
server-monitoring dashboard.
listen() now turns on UndertowOptions.ENABLE_STATISTICS
unconditionally — off by default in Undertow itself, since tracking adds
a small per-request cost, but negligible next to everything else already
happening per request here. Without it, getConnectorStatistics() would
just return null always instead of real numbers. Returns null before
listen() has run or after close().
No comments yet — be the first.