--- name: boxlang-core-dev-interceptors description: "Use this skill when creating BoxLang interceptors: Observer/Intercepting Filter patterns, interceptor pools, BoxLang class vs Java interceptors, lambda interceptors, registration via BIFs/InterceptorService/ModuleConfig, interception points, and announcing custom events." --- # BoxLang Interceptors > BoxLang is the AI-native software productivity platform for building, modernizing and running applications, with developers and AI agents working together. ## Overview BoxLang's interceptor system implements the **Observer** and **Intercepting Filter** design patterns. Interceptors listen for named events (interception points) announced by the runtime, modules, or application code. They enable cross-cutting concerns like logging, security, AOP, routing, and content manipulation without modifying core code. ## Interceptor Pools Three pools exist in the runtime. Each pool is an independent event emitter: | Pool | Scope | Use Case | |------|-------|---------| | **Global Runtime** | Entire BoxLang process | Module events, parse events, global AOP | | **Application Request Listener** | Per-application request lifecycle | Auth, routing, request logging | | **CacheProviders** | Per cache instance | Cache event hooks | ## Creating a BoxLang Class Interceptor Method names in the class directly correspond to interception point names: ```boxlang // interceptors/RequestLogger.bx class { // Auto-injected by the runtime property name="name" setter="false" property name="properties" setter="false" property name="log" setter="false" // module logger property name="interceptorService" setter="false" property name="boxRuntime" setter="false" property name="moduleRecord" setter="false" // if module-based /** * Called when the interceptor is registered. * Use this to validate properties and set up resources. */ function configure() { // Access settings passed at registration time log.info( "RequestLogger interceptor configured. Level: #properties.logLevel ?: 'INFO'#" ) } // --------------------------------------------------------------- // Interception point methods — name must EXACTLY match the event // --------------------------------------------------------------- /** * Fires before every HTTP request. * @event Struct with request context data (mutable) */ function onRequestStart( struct event={} ) { log.info( "→ #cgi.request_method# #cgi.script_name##cgi.query_string ?: ''#" ) event.requestStartTime = getTickCount() } /** * Fires after every HTTP request. */ function onRequestEnd( struct event={} ) { var elapsed = getTickCount() - (event.requestStartTime ?: getTickCount()) log.info( "← #cgi.script_name# completed in #elapsed#ms" ) } /** * Fires when an unhandled exception occurs. */ function onException( struct event={} ) { var ex = event.exception ?: {} log.error( "Exception: #ex.message ?: 'unknown'# | #ex.stackTrace ?: ''#" ) } } ``` ## Creating a Java Interceptor ```java // src/main/java/com/example/interceptors/RequestLogger.java package com.example.interceptors; import ortus.boxlang.runtime.events.BaseInterceptor; import ortus.boxlang.runtime.events.InterceptionPoint; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.IStruct; @ortus.boxlang.runtime.events.Interceptor public class RequestLogger extends BaseInterceptor { @Override public void configure() { // this.properties is available for config // this.log is available for logging // this.interceptorService is available // this.boxRuntime is available } // @InterceptionPoint marks this method as an event handler @InterceptionPoint public void onRequestStart( IStruct event ) { String method = (String) event.getOrDefault( Key.of("requestMethod"), "GET" ); this.log.info( "Request started: " + method ); event.put( Key.of("startTime"), System.currentTimeMillis() ); } @InterceptionPoint public void onRequestEnd( IStruct event ) { Long startTime = (Long) event.getOrDefault( Key.of("startTime"), System.currentTimeMillis() ); long elapsed = System.currentTimeMillis() - startTime; this.log.info( "Request completed in " + elapsed + "ms" ); } @InterceptionPoint public void preFunctionInvoke( IStruct event ) { // event contains: functionName, arguments, context String fnName = event.getAsString( Key.of("functionName") ); this.log.debug( "Invoking function: " + fnName ); } } ``` ## Lambda and Closure Interceptors Closures/lambdas **must** explicitly specify which events they handle: ```boxlang // Closure interceptor — must specify states var myListener = function( struct event={} ) { writeOutput( "Request started!" ) } // Register for specific events boxRegisterInterceptor( myListener, ["onRequestStart", "onRequestEnd"] ) // Lambda form boxRegisterInterceptor( (event) -> logEvent( event ), ["onHTTPRequest"] ) ``` ## Registering Interceptors ### Method 1: ModuleConfig.bx (Recommended for Modules) ```boxlang // ModuleConfig.bx class { this.interceptors = [ { // Full invocation path to the interceptor class class : "#moduleRecord.invocationPath#.interceptors.RequestLogger", properties : { logLevel : "INFO", maxErrors : 100 } }, { class : "#moduleRecord.invocationPath#.interceptors.SecurityFilter", properties : { enabled: true } } ] } ``` ### Method 2: BIF Registration ```boxlang // Global runtime interceptor boxRegisterInterceptor( new interceptors.MyInterceptor() ) // With properties boxRegisterInterceptor( new interceptors.AuditLog(), [], // states: empty = listen to all matching methods { logPath: "/var/log/audit.log" } ) // Application request listener (web context) boxRegisterRequestInterceptor( new interceptors.AuthFilter() ) ``` ### Method 3: InterceptorService API (Java) ```java InterceptorService service = BoxRuntime.getInstance().getInterceptorService(); // Register a Java-based interceptor service.register( new RequestLogger() ); // Register with properties IStruct props = new Struct(); props.put( Key.of("logLevel"), "DEBUG" ); service.register( new RequestLogger(), props ); // Register a BoxLang class IClassRunnable bxInterceptor = (IClassRunnable) context.loadClass("interceptors.MyBxInterceptor"); service.register( bxInterceptor ); // Register a closure for specific states service.register( (IInterceptorLambda) event -> System.out.println("Event: " + event), Key.of("onRequestStart"), Key.of("onRequestEnd") ); ``` ## Common Interception Points All canonical event names are defined in the `BoxEvent` enum: `ortus.boxlang.runtime.events.BoxEvent` Use `BoxEvent.ON_REQUEST_START.key()` in Java, or the string name in BoxLang. ### Runtime Events | Event | When | Payload Keys | |---|---|---| | `onRuntimeStart` | Runtime fully started | `runtime` | | `onRuntimeShutdown` | Before shutdown | `runtime` | | `onRuntimeConfigurationLoad` | Config loaded | `config` | | `onRuntimeBoxContextStartup` | First context created | `context` | | `onServerScopeCreation` | Server scope initialized | `serverScope` | | `onConfigurationLoad` | Config file loaded | `config` | | `onConfigurationOverrideLoad` | Override config loaded | `config` | | `onParse` | Before source is parsed | `source`, `result` | | `onMissingMapping` | Mapping cannot be resolved | `mapping` | | `onPreSourceInvoke` | Before any source file runs | `context`, `source` | | `onPostSourceInvoke` | After any source file runs | `context`, `source` | ### Class Instantiation Events (v1.18+) | Event | When | Payload Keys | |---|---|---| | `afterBoxClassCreation` | Instance defined, before `init()`. Also fires for `noInit` creations (`createObject()`, deserialization). Never fires for super classes in an `extends` chain. | `instance`, `className`, `noInit`, `context` | | `afterBoxClassInit` | After `init()` or the implicit constructor completes. Skipped for `noInit` creations and when `init()` throws. | `instance`, `result`, `className`, `context` | ### Module Events | Event | When | |---|---| | `onModuleServiceStartup` | Module service starts | | `onModuleServiceShutdown` | Module service stops | | `afterModuleRegistrations` | All modules registered | | `preModuleRegistration` | Before a module is registered | | `postModuleRegistration` | After a module is registered | | `afterModuleActivations` | All modules activated | | `preModuleLoad` | Before a module is loaded | | `postModuleLoad` | After a module is loaded | | `preModuleUnload` | Before a module is unloaded | | `postModuleUnload` | After a module is unloaded | ### Application / Request Events (Web Context) | Event | When | Notes | |---|---|---| | `onApplicationStart` | Application started | | | `onApplicationEnd` | Application ended | | | `onApplicationRestart` | Application restarted | | | `onApplicationDefined` | Application.bx found | | | `beforeApplicationListenerLoad` | Before Application.bx loads | | | `afterApplicationListenerLoad` | After Application.bx loads | | | `onRequestStart` | Before request starts | Fires before Application.bx lifecycle | | `onRequest` | Main request handling | | | `onRequestEnd` | After request completes | | | `onClassRequest` | Class/component requested | | | `onRequestFlushBuffer` | Output buffer flushed | | | `onSessionStart` | New session created | Session struct available | | `onSessionEnd` | Session expires or is invalidated | Session data available | | `onSessionCreated` | Session object created | | | `onSessionDestroyed` | Session object destroyed | | | `onError` | Unhandled exception | | | `onMissingTemplate` | Template not found | | | `onAbort` | Execution aborted | | | `onRequestContextConfig` | Request context configured | | ### HTTP Events | Event | When | |---|---| | `onHTTPRequest` | Every HTTP request | | `onHTTPRawResponse` | Before raw HTTP response sent | | `onHTTPResponse` | Before HTTP response sent | | `onWebExecutorRequest` | Web executor request (route modification) | ### BIF / Component Lifecycle Events | Event | When | |---|---| | `onBIFInstance` | BIF instantiated | | `onBIFInvocation` | Before BIF invoked | | `postBIFInvocation` | After BIF invoked | | `onComponentInstance` | Component instantiated | | `onComponentInvocation` | Component invoked | | `onCacheComponentAction` | Cache component action | | `onFileComponentAction` | File component action | | `onCreateObjectRequest` | `createObject()` called | | `afterDynamicObjectCreation` | Java object wrapped in DynamicObject | ### Template / Function Events | Event | When | Payload | |---|---|---| | `preTemplateInvoke` | Before template executes | `template`, `context` | | `postTemplateInvoke` | After template executes | `template`, `context` | | `preFunctionInvoke` | Before any function call | `functionName`, `arguments`, `context` | | `postFunctionInvoke` | After any function call | `functionName`, `result` | | `onFunctionException` | Function throws exception | `functionName`, `exception` | ### Query / Transaction Events | Event | When | |---|---| | `onQueryBuild` | Before query is built | | `preQueryExecute` | Before query executes | | `postQueryExecute` | After query executes | | `queryAddRow` | Row added to query | | `onTransactionBegin` | Transaction started | | `onTransactionEnd` | Transaction ended | | `onTransactionAcquire` | Connection acquired | | `onTransactionRelease` | Connection released | | `onTransactionCommit` | Transaction committed | | `onTransactionRollback` | Transaction rolled back | | `onTransactionSetSavepoint` | Savepoint created | ### Cache Events | Event | When | |---|---| | `afterCacheElementInsert` | Item inserted | | `beforeCacheElementRemoved` | Before item removed | | `afterCacheElementRemoved` | After item removed | | `afterCacheElementUpdated` | Item updated | | `afterCacheClearAll` | Cache cleared | | `afterCacheRegistration` | Cache provider registered | | `afterCacheRemoval` | Cache provider removed | | `beforeCacheRemoval` | Before cache provider removed | | `beforeCacheReplacement` | Before cache entry replaced | | `beforeCacheShutdown` | Before cache shuts down | | `afterCacheShutdown` | After cache shuts down | | `afterCacheServiceStartup` | CacheService started | | `beforeCacheServiceShutdown` | Before CacheService shuts down | | `afterCacheServiceShutdown` | After CacheService shuts down | ### Scheduler Events | Event | When | |---|---| | `onSchedulerStartup` | Scheduler started | | `onSchedulerShutdown` | Scheduler stopped | | `onSchedulerRestart` | Scheduler restarted | | `schedulerBeforeAnyTask` | Before any task runs | | `schedulerAfterAnyTask` | After any task runs | | `schedulerOnAnyTaskSuccess` | Task succeeded | | `schedulerOnAnyTaskError` | Task threw an error | | `onSchedulerServiceStartup` | SchedulerService started | | `onSchedulerServiceShutdown` | SchedulerService stopped | | `onAllSchedulersStarted` | All schedulers running | | `onSchedulerRemoval` | Scheduler removed | | `onSchedulerRegistration` | Scheduler registered | ### Datasource Events | Event | When | |---|---| | `onDatasourceConfigLoad` | Datasource config loaded | | `onDatasourceServiceStartup` | DatasourceService started | | `onDatasourceServiceShutdown` | DatasourceService stopped | | `onDatasourceStartup` | Datasource initialized | ### Object Marshaller Events | Event | When | |---|---| | `beforeObjectMarshallSerialize` | Before serialization | | `afterObjectMarshallSerialize` | After serialization | | `beforeObjectMarshallDeserialize` | Before deserialization | | `afterObjectMarshallDeserialize` | After deserialization | | `onJSONQuerySerialize` | Query serialized to JSON | ### Dump / Other Events | Event | When | |---|---| | `onBXDump` | `writeDump()` called | | `onMissingDumpOutput` | No dump output handler found | | `logMessage` | Log message emitted | ## Announcing Custom Events You can announce your own events for other interceptors to listen to: ```boxlang // Announce a custom event with a data payload boxAnnounce( "onUserRegistered", { userId : newUser.getId(), email : newUser.getEmail(), source : "web" }) // Listeners elsewhere can react to this: // function onUserRegistered( struct event={} ) { // sendWelcomeEmail( event.email ) // createDefaultWorkspace( event.userId ) // } ``` ```java // Java: announce via InterceptorService IStruct data = new Struct(); data.put( Key.of("userId"), userId ); data.put( Key.of("email"), email ); BoxRuntime.getInstance() .getInterceptorService() .announce( Key.of("onUserRegistered"), data ); ``` ## Use Cases ### A/B Routing and Feature Flags (pre-request) ```boxlang // interceptors/FeatureFlagRouter.bx // v1.11+: fires BEFORE Application.bx onRequestStart — enables rerouting class { function onRequestStart( struct event={} ) { var userId = session.userId ?: "" if ( len(userId) && featureFlags.isEnabled("new-ui", userId) ) { event.overridePath = "/new-ui" & cgi.path_info } } } ``` ### Maintenance Mode ```boxlang // interceptors/MaintenanceMode.bx class { function onRequestStart( struct event={} ) { if ( application.maintenanceMode && !isAdminIp( cgi.remote_addr ) ) { include "maintenance.bxm" abort } } } ``` ### AOP Method Tracing ```boxlang // interceptors/MethodTracer.bx class { function preFunctionInvoke( struct event={} ) { var fn = event.functionName ?: "unknown" var start = getTickCount() event.traceStart = start log.debug( "→ #fn# called" ) } function postFunctionInvoke( struct event={} ) { var elapsed = getTickCount() - (event.traceStart ?: getTickCount()) log.debug( "← #event.functionName ?: 'unknown'# completed in #elapsed#ms" ) } } ``` ### Content Manipulation ```boxlang // interceptors/HtmlMinifier.bx class { function onRequestEnd( struct event={} ) { // Minify HTML output before sending to client var output = event.output ?: "" if ( len(output) ) { event.output = minifyHtml( output ) } } } ``` ## Unregistering Interceptors ```boxlang // Unregister by reference boxUnregisterInterceptor( myInterceptorInstance ) // Via InterceptorService (Java) BoxRuntime.getInstance().getInterceptorService().unregister( myInterceptor ) ``` ## References - [Interceptors](https://boxlang.ortusbooks.com/boxlang-framework/interceptors) - [BaseInterceptor](https://github.com/ortus-boxlang/BoxLang/blob/development/src/main/java/ortus/boxlang/runtime/events/BaseInterceptor.java) - [InterceptorService](https://github.com/ortus-boxlang/BoxLang/blob/development/src/main/java/ortus/boxlang/runtime/services/InterceptorService.java) - [BoxLang Source](https://github.com/ortus-boxlang/BoxLang)