From f395495193ec97796658ca516e6b40c35c23c76a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 03:16:48 +0000 Subject: [PATCH] docs: add What's New With 8.2.0 and fix three doc gaps found in the review Adds a consolidated release-notes page covering everything shipped since v8.1.0: route middleware, HTTP caching primitives, first-class SSE, AI routing conversational context, a security fix to HTTP method spoofing, and Renderer/routing performance work. Also fixes three gaps found while grounding that page against the real merged code: http-method-spoofing.md described the pre-fix, unrestricted _method override behavior (now a documented security hazard if left uncorrected); view-caching.md never mentioned the new viewDiscoveryCaching setting; and announcing-interceptions.md never documented that announce() now returns true/false to report whether an interceptor short-circuited the chain. --- SUMMARY.md | 1 + .../release-history/whats-new-with-8.2.0.md | 144 ++++++++++++++++++ .../custom-events/announcing-interceptions.md | 14 ++ .../layouts-and-views/views/view-caching.md | 15 ++ the-basics/routing/http-method-spoofing.md | 24 ++- 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 readme/release-history/whats-new-with-8.2.0.md diff --git a/SUMMARY.md b/SUMMARY.md index acd0cfcd..d9b65fc9 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -3,6 +3,7 @@ * [Introduction](README.md) * [Contributing Guide](readme/contributing-guide.md) * [Release History](readme/release-history/README.md) + * [What's New With 8.2.0](readme/release-history/whats-new-with-8.2.0.md) * [What's New With 8.1.0](readme/release-history/whats-new-with-8.1.0.md) * [What's New With 8.0.0](readme/release-history/whats-new-with-8.0.0.md) * [Upgrading to ColdBox 8](readme/upgrading-to-coldbox-8.md) diff --git a/readme/release-history/whats-new-with-8.2.0.md b/readme/release-history/whats-new-with-8.2.0.md new file mode 100644 index 00000000..2f37f518 --- /dev/null +++ b/readme/release-history/whats-new-with-8.2.0.md @@ -0,0 +1,144 @@ +--- +description: August 17, 2026 +--- + +# What's New With 8.2.0 + +ColdBox 8.2.0 is a feature release focused on request-level composition and control: route-scoped middleware, standards-based HTTP caching, first-class Server-Sent Events, and conversational context for AI routing โ€” plus a security hardening fix, a handful of correctness fixes, and hot-path performance work in the Renderer and routing layers. + +## Major Highlights + +### ๐Ÿงต Route-Scoped Middleware โ€” `.middleware()` + +Routes can now carry their own middleware chain instead of relying solely on app-wide interceptors. `.middleware()` attaches a closure, a WireBox ID, or any object with a method named after the interception point (`preProcess` by default, or `postProcess`) to a single route โ€” reusing the same dispatch mechanism ColdBox interceptors already use, just scoped to one route. + +```javascript +route( "/admin/:action" ) + .middleware( function( event, rc, prc ){ + if ( !auth.isLoggedIn() ) { + event.relocate( "login" ); + return true; // short-circuits the rest of this route's middleware + } + } ) + .toHandler( "admin" ); +``` + +Two companion features round this out: + +* **`middlewareGroup( name, [ ...targets ] )`** โ€” register a named, reusable bundle once, then reference it by name from `.middleware()` or a `group()`'s `middleware` option, instead of repeating the same target list everywhere. +* **`.withoutMiddleware( target )`** โ€” opt a single route out of middleware it would otherwise inherit, by target name, by the group name it expanded from, or `"*"` for everything. + +```javascript +middlewareGroup( "api", [ "RequireApiKey", "RateLimiter" ] ); + +group( { pattern : "/api", middleware : [ "api" ] }, function(){ + route( "/users" ).toHandler( "users" ); // runs "api" + route( "/health" ).withoutMiddleware( "api" ).toHandler( "health" ); // opts out +} ); +``` + +See [Route Middleware](../../the-basics/routing/routing-dsl/middleware.md) and [Middleware Groups & Exclusions](../../the-basics/routing/routing-dsl/middleware-groups.md). + +### ๐Ÿ—„๏ธ HTTP Caching Primitives โ€” ETag, Last-Modified, Cache-Control + +Standards-based conditional-GET support, usable from any handler: + +```javascript +function show( event, rc, prc ){ + prc.product = productService.get( rc.id ); + + if ( event.etag( prc.product.getHash() ) ) { + return; // 304 already sent - nothing left to do + } + + event.setView( "products/show" ); +} +``` + +* **`event.etag()`** / **`event.lastModified()`** โ€” set the corresponding header and short-circuit with a `304 Not Modified` on a match. Never short-circuits an unsafe HTTP method. +* **`event.cacheControl()`** โ€” build a `Cache-Control` header from a directives struct. +* **`Response.withETag()`** / **`Response.withCacheControl()`** โ€” the same primitives, as fluent methods on the REST `Response` object. +* An already `cache="true"` event handler action can opt into an **automatically computed** `ETag`/`Last-Modified` with new `etag`/`etagWeak`/`lastModified`/`cacheControl` annotations, piggybacking on event caching with no extra per-request work. + +See [HTTP Caching](../../digging-deeper/http-caching.md). + +### ๐Ÿ“ก First-Class Server-Sent Events (BoxLang) + +Streaming is now a first-class concern instead of something bolted onto AI routing. `event.sse()` takes over the response and hands your callback an `SSEEmitter` with `send()`, `sendView()`, `sendData()`, `sendError()`, keep-alives, and graceful disconnect handling: + +```javascript +function ticker( event, rc, prc ){ + event.sse( ( emitter ) => { + while ( emitter.isOpen() ) { + emitter.send( { "ts" : now() }, "tick" ); + sleep( 1000 ); + } + } ); +} +``` + +For routes that always stream, `Router.toSSE()` mirrors `toResponse()`. Three new interception points (`preSSEConnection`, `postSSEConnection`, `onSSEError`) let you reject a connection before it opens, observe stream completion, or react to mid-stream errors, and a new `this.sse` settings block controls keep-alive interval, reconnect hints, and CORS defaults. + +See [Server-Sent Events](../../the-basics/event-handlers/server-sent-events.md) and [Streaming Routes (SSE)](../../the-basics/routing/routing-dsl/sse-routes.md). + +### ๐Ÿค– AI Routing Gets Conversational Context + +`toAi()`'s `invoke`, `stream`, and `batch` sub-routes now resolve `userId`, `conversationId`, and `threadId` from the request body and thread them through to the runnable via `options`: + +* **`userId`** defaults to the framework's own session/request tracking identifier when not supplied +* **`conversationId`** is passed through only if supplied - no default is invented +* **`threadId`** is generated if not supplied, and is **always** echoed back - in the JSON response, an `X-Thread-Id` header, and a leading `event: thread` SSE frame on `/stream` (since browser `EventSource` clients can't read response headers) + +```javascript +// POST /api/chat/invoke { "input": "hi", "threadId": "t-123" } +// โ†’ runnable.run( "hi", {}, { userId: "", threadId: "t-123" } ) +// โ†’ { "output": ..., "success": true, "threadId": "t-123" } +``` + +This release also corrects the `toAi()` reference documentation, which had drifted from the actual `run()`/`stream()`-based `IAiRunnable` interface and request/response shapes. See [AI Routing](../../the-basics/routing/routing-dsl/ai-routing.md). + +### ๐Ÿ”’ Hardened HTTP Method Spoofing + +The `_method` form-field override (`GET`/`POST` browsers use to fake `PUT`/`PATCH`/`DELETE`) is now only honored when the *original* transport-level request is a `POST`. Previously, a plain `GET` request carrying `?_method=DELETE` was silently treated as a `DELETE` โ€” enabling CSRF-style attacks via a link, an `` tag, a crawler, or a browser prefetch. A new `event.getOriginalHTTPMethod()` returns the raw, un-spoofed verb when you need it. See [HTTP Method Spoofing](../../the-basics/routing/http-method-spoofing.md). + +### โšก Renderer & Routing Performance + +* A new `viewDiscoveryCaching` setting (on by default, independent of `viewCaching`) caches the filesystem work that locates view/layout files, benefiting every render regardless of whether view *output* caching is enabled. See [View Discovery Caching](../../the-basics/layouts-and-views/views/view-caching.md#view-discovery-caching). +* Several hot-path costs eliminated in `HandlerService`, `RoutingService`, and `Router` โ€” a per-request settings lookup that should have been cached at configuration time, a redundant filesystem check in view dispatch detection, and route response placeholders now pre-parsed once at registration instead of re-parsed by regex on every matching request. +* The interceptor chain now reports short-circuiting: `announce()` returns `true` on the synchronous path when an interceptor short-circuited the chain, `false` otherwise - previously this was undetectable. See [Announcing Interceptions](../../the-basics/interceptors/custom-events/announcing-interceptions.md#detecting-a-short-circuit). + +## Release Notes + +{% tabs %} +{% tab title="ColdBox" %} +### New Features + +[COLDBOX-1415](https://ortussolutions.atlassian.net/browse/COLDBOX-1415) HTTP caching primitives - ETag, Last-Modified, Cache-Control + +[COLDBOX-1416](https://ortussolutions.atlassian.net/browse/COLDBOX-1416) Route-scoped middleware via existing interceptor points + +[COLDBOX-1417](https://ortussolutions.atlassian.net/browse/COLDBOX-1417) Conversational context (userId/conversationId/threadId) on `toAi()` routes + +First-class Server-Sent Events streaming - `event.sse()`, `SSEEmitter`, `Router.toSSE()`, interception points, `this.sse` settings ([#676](https://github.com/ColdBox/coldbox-platform/pull/676)) + +### Improvements + +[COLDBOX-1406](https://ortussolutions.atlassian.net/browse/COLDBOX-1406) HTTP method spoofing restricted to `POST` requests only; new `getOriginalHTTPMethod()` + +New `viewDiscoveryCaching` setting plus Renderer hot-path caching for view/layout discovery ([#664](https://github.com/ColdBox/coldbox-platform/pull/664)) + +Hot-path performance optimizations in `HandlerService`, `RoutingService`, and `Router` ([#665](https://github.com/ColdBox/coldbox-platform/pull/665)) + +Interceptor chain now reports and honors short-circuiting via `announce()`'s return value; new `getRouteDefinitionKeys()` route-table introspection helper ([#677](https://github.com/ColdBox/coldbox-platform/pull/677)) + +### Bugs + +[COLDBOX-1407](https://ortussolutions.atlassian.net/browse/COLDBOX-1407) `url.results` collision breaking `cache.getOrSet()` on any page loaded with `?results=...` + +[COLDBOX-1411](https://ortussolutions.atlassian.net/browse/COLDBOX-1411) `this.EVENT_CACHE_SUFFIX` closures were evaluated once and frozen instead of per-request, letting different requests share a cache key + +`DataMarshaller` component made thread-safe ([#672](https://github.com/ColdBox/coldbox-platform/pull/672)) + +Fixed a startup typo in `Bootstrap.cfc` ([#667](https://github.com/ColdBox/coldbox-platform/pull/667)) +{% endtab %} +{% endtabs %} diff --git a/the-basics/interceptors/custom-events/announcing-interceptions.md b/the-basics/interceptors/custom-events/announcing-interceptions.md index 463a5623..686fe02a 100644 --- a/the-basics/interceptors/custom-events/announcing-interceptions.md +++ b/the-basics/interceptors/custom-events/announcing-interceptions.md @@ -18,3 +18,17 @@ controller.getInterceptorService().announce( "onRecordInsert", {} ); ``` > **Hint** Announcing events can also get some asynchronous love, read the [Interceptor Asynchronicity](../interceptor-asynchronicity/) for some asynchronous love. + +## Detecting a Short-Circuit + +On the default synchronous path, `announce()` returns `true` if an interceptor short-circuited the chain by returning `true` from its handler, `false` otherwise (interceptors that never return a boolean, and points with no registered interceptors, resolve to `false`). Use this to detect that an interceptor consumed or rejected the announcement: + +```javascript +if ( controller.getInterceptorService().announce( "preSSEConnection", data ) ) { + // an interceptor returned true - the chain was short-circuited +} +``` + +{% hint style="info" %} +This only applies to the synchronous path. `async`/`asyncAll` announcements return a thread structure report instead, as documented in [Interceptor Asynchronicity](../interceptor-asynchronicity/). +{% endhint %} diff --git a/the-basics/layouts-and-views/views/view-caching.md b/the-basics/layouts-and-views/views/view-caching.md index b1dae4c2..779850a7 100644 --- a/the-basics/layouts-and-views/views/view-caching.md +++ b/the-basics/layouts-and-views/views/view-caching.md @@ -64,3 +64,18 @@ coldbox = { viewCaching = false } ``` + +## View Discovery Caching + +`viewDiscoveryCaching` is a separate setting from `viewCaching` above - it's on by default and controls a different thing entirely. `viewCaching` caches a view's rendered **output**; `viewDiscoveryCaching` caches the **path resolution** work the Renderer does to locate a view/layout file on disk (module fallback checks, extension detection, etc) - pure overhead that never changes once a file exists at a given path. + +```javascript +coldbox = { + // Cache the filesystem lookups that locate view/layout files. Default: true + viewDiscoveryCaching = true +} +``` + +{% hint style="info" %} +Turning this off does not disable `viewCaching` - the two settings are independent. You'd disable `viewDiscoveryCaching` only if you're dynamically adding view files to a running application and need every request to re-check the filesystem for them. +{% endhint %} diff --git a/the-basics/routing/http-method-spoofing.md b/the-basics/routing/http-method-spoofing.md index 429e8f25..91275b81 100644 --- a/the-basics/routing/http-method-spoofing.md +++ b/the-basics/routing/http-method-spoofing.md @@ -2,7 +2,7 @@ Although we have access to all the HTTP verbs, modern browsers still only support **GET** and **POST**. With ColdBox and HTTP Method Spoofing, you can take advantage of **all** the HTTP verbs in your web forms. -By convention, ColdBox will look for an `_method` field in the **FORM** scope. If one exists, the value of this field is used as the HTTP method instead of the method from the execution. For instance, the following block of code would execute with the **DELETE** action instead of the **POST** action: +By convention, ColdBox will look for an `_method` field in the **FORM** scope. If one exists, and the transport-level request is a **POST**, its value is used as the HTTP method instead of `POST` โ€” for `PUT`, `PATCH`, or `DELETE` targets only. For instance, the following block of code would execute with the **DELETE** action instead of the **POST** action: ```markup @@ -22,3 +22,25 @@ You can manually add these `_method` fields yourselves, or you can take advantag #html.endForm()# ``` + +{% hint style="danger" %} +**Security:** the `_method` override is only honored when the *original* transport-level request method is `POST`. A `GET` request carrying `?_method=DELETE` is returned as `GET` unchanged โ€” this prevents a plain link, an `` tag, a crawler, or a browser prefetch from silently triggering a destructive HTTP verb. Only `PUT`, `PATCH`, and `DELETE` are honored as override targets; `?_method=GET` on a `POST` request is likewise ignored and the original `POST` is kept. +{% endhint %} + +| Original method | `_method` | `event.getHTTPMethod()` result | +| ---------------- | ---------- | -------------------------------- | +| `GET` | `DELETE` | `GET` โ€” override ignored | +| `HEAD` | `DELETE` | `HEAD` โ€” override ignored | +| `POST` | `DELETE` | `DELETE` โœ“ | +| `POST` | `PUT` | `PUT` โœ“ | +| `POST` | `PATCH` | `PATCH` โœ“ | +| `POST` | `GET` | `POST` โ€” invalid override ignored | + +## Getting the Original Method + +`event.getHTTPMethod()` returns the *effective* method after spoofing is applied - the one your routes and `withVerbs()`/`allowedMethods` checks match against. If you need the raw, un-spoofed transport-level method instead (for logging, auditing, or a security interceptor), use `event.getOriginalHTTPMethod()`: + +```javascript +event.getHTTPMethod(); // "DELETE" - after _method spoofing is applied +event.getOriginalHTTPMethod(); // "POST" - the real transport-level verb, always +```