Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
144 changes: 144 additions & 0 deletions readme/release-history/whats-new-with-8.2.0.md
Original file line number Diff line number Diff line change
@@ -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: "<session id>", 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 `<img>` 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 %}
14 changes: 14 additions & 0 deletions the-basics/interceptors/custom-events/announcing-interceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
15 changes: 15 additions & 0 deletions the-basics/layouts-and-views/views/view-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
24 changes: 23 additions & 1 deletion the-basics/routing/http-method-spoofing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<cfoutput>
Expand All @@ -22,3 +22,25 @@ You can manually add these `_method` fields yourselves, or you can take advantag
#html.endForm()#
</cfoutput>
```

{% 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 `<img>` 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
```