All Articles

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

VersionHeadline
0.1.15Fixed a class of 500 crashes on ordinary empty-value input (?q=, empty cookies, empty multipart fields)
0.1.16Trusted-proxy IP resolution hardened for real multi-hop platforms (DigitalOcean + Cloudflare)
0.1.17Fixed session data loss against any real (non-memory) session store
0.1.18Added response outcome accessors for logging middleware
0.1.19Added opt-out session store writes (resave/saveUninitialized)
0.1.20Fixed a trailing-slash bug reachable through nearly every request
0.2.0Breaking: server transport switched from the JDK's HttpServer to Undertow
0.2.1Added 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.