How to Implement Middleware Chains for HTTP Triggers Using `middleware_function_ids` in III
To implement middleware chains for HTTP triggers in III, register your trigger with the middleware_function_ids array containing function IDs, where each middleware must return either { "action": "continue" } to proceed or { "action": "respond", "response": {...} } to short-circuit the request.
The iii open-source framework (iii-hq/iii) treats HTTP triggers as routes that execute sequential middleware functions before reaching the final handler. By leveraging the middleware_function_ids configuration field, you can build robust request processing pipelines that handle authentication, logging, validation, and early termination directly within the Rust-based engine.
Understanding the Middleware Architecture
III's architecture separates trigger registration from request execution, allowing you to declaratively specify middleware chains that the engine stores and invokes at runtime.
Trigger Registration with middleware_function_ids
When registering an HTTP trigger via the SDK, you include the middleware_function_ids field in the trigger configuration. This array defines the ordered list of middleware functions that will process each request:
iii.registerTrigger({
type: 'http',
function_id: handlerFn.id,
config: {
api_path: '/orders',
http_method: 'POST',
middleware_function_ids: ['middleware::request-logger', 'middleware::auth'],
},
})
The SDK serializes this configuration and transmits it to the engine over WebSocket protocol.
The HTTP Trigger Schema Definition
The Rust engine validates incoming trigger configurations against the schema defined in engine/src/trigger_formats.rs. The HttpTriggerConfig struct explicitly includes the middleware chain definition:
// engine/src/trigger_formats.rs (lines 24-33)
pub struct HttpTriggerConfig {
pub api_path: String,
pub http_method: String,
pub middleware_function_ids: Vec<String>, // Ordered middleware chain
// ... other fields
}
This schema ensures that middleware references are stored as Vec<String> and validated at registration time.
How the Middleware Chain Executes at Runtime
Once registered, the middleware chain executes within the engine's request handling flow, specifically in engine/src/workers/rest_api/views.rs.
Router Storage and Lookup
The engine stores middleware references in the PathRouter struct defined in engine/src/workers/rest_api/api_core.rs (lines 39-46):
pub struct PathRouter {
pub function_id: String,
pub middleware_function_ids: Vec<String>, // Mirrored from trigger config
pub api_path: String,
}
When a request arrives, the dynamic_handler performs the following sequence:
- Global middleware execution from REST-API configuration
- Per-route middleware iteration over
middleware_function_ids - Handler invocation (only if all middleware returns
continue)
The Execution Loop in dynamic_handler
The core logic resides in engine/src/workers/rest_api/views.rs (lines 504-526):
// Per-route middleware (runs after condition check)
for mw_fn_id in &middleware_function_ids {
let mw_input = build_middleware_input(
&api_request_value.path_params,
&api_request_value.query_params,
&headers,
method.as_str(),
);
match execute_middleware(&engine, mw_fn_id, mw_input, rest_api_config.default_timeout).await {
Ok(MiddlewareResult::Continue) => {}
Err(response) => {
// short-circuit: clean up channels and return the middleware response
channel_mgr.remove_channel(&req_ch_id);
channel_mgr.remove_channel(&res_ch_id);
return response;
}
}
}
Each middleware receives a MiddlewareInput containing query parameters, path parameters, headers, and the HTTP method. The engine passes this input to execute_middleware, which handles the function invocation and response parsing.
Implementing Middleware Functions
Middleware functions follow a strict contract: they must return a JSON object specifying the desired action.
The Middleware Contract
Valid middleware responses must include an action field with one of two values:
{ "action": "continue" }— Proceed to the next middleware or the final handler{ "action": "respond", "response": { "status_code": <number>, "body": <json> } }— Stop execution and return immediately
TypeScript SDK Implementation
Register middleware functions using the SDK before referencing them in trigger configuration:
import { iii } from '@iii-sdk/core';
// Request logging middleware
iii.registerFunction('middleware::request-logger', async (req) => {
console.log('Incoming request', req);
return { action: 'continue' };
});
// Authentication middleware with short-circuit capability
iii.registerFunction('middleware::auth', async (req) => {
if (!req.headers.authorization) {
return {
action: 'respond',
response: {
status_code: 401,
body: { error: 'Missing auth' }
},
};
}
return { action: 'continue' };
});
// Main handler
const handler = iii.registerFunction('orders::create', async (req) => ({
status_code: 201,
body: { message: 'order created' },
}));
// Register trigger with middleware chain
iii.registerTrigger({
type: 'http',
function_id: handler.id,
config: {
api_path: '/orders',
http_method: 'POST',
middleware_function_ids: [
'middleware::request-logger',
'middleware::auth',
],
},
});
Rust SDK Implementation
Middleware can also be written in Rust using the III SDK:
use iii_sdk::FunctionInput;
#[iii::function]
async fn auth_mw(input: FunctionInput) -> iii_sdk::FunctionResult {
let headers = input.get("headers").unwrap();
if headers.get("authorization").is_none() {
Ok(json!({
"action": "respond",
"response": {
"status_code": 403,
"body": {"error": "Forbidden"},
}
}))
} else {
Ok(json!({ "action": "continue" }))
}
}
Short-Circuiting Requests for Early Exit
The middleware chain supports early termination, allowing you to reject unauthorized requests or validate inputs before expensive handler execution.
When a middleware returns action: 'respond', the engine:
- Immediately halts middleware iteration
- Cleans up request/response channels via
channel_mgr.remove_channel() - Returns the middleware's response directly to the client
This design saves computational resources by preventing unnecessary handler invocations for failed authentication or validation checks. The end-to-end test suite in engine/tests/http_middleware_e2e.rs verifies both continuation and short-circuit behaviors across the full request lifecycle.
Testing Your Middleware Chain
Verify middleware execution order and short-circuiting using the Node.js test utilities:
import { describe, it, expect } from 'vitest';
import { iii, httpRequest, execute } from './utils';
describe('HTTP Middleware Chain', () => {
it('executes middleware in declared order', async () => {
const executionOrder: string[] = [];
iii.registerFunction('mw::first', async () => {
executionOrder.push('first');
return { action: 'continue' };
});
iii.registerFunction('mw::second', async () => {
executionOrder.push('second');
return { action: 'continue' };
});
iii.registerFunction('handler::test', async () => {
executionOrder.push('handler');
return { status_code: 200, body: {} };
});
iii.registerTrigger({
type: 'http',
function_id: 'handler::test',
config: {
api_path: '/test',
http_method: 'GET',
middleware_function_ids: ['mw::first', 'mw::second'],
},
});
const resp = await execute(() => httpRequest('GET', '/test'));
expect(resp.status).toBe(200);
expect(executionOrder).toEqual(['first', 'second', 'handler']);
});
});
Summary
middleware_function_idsis defined inengine/src/trigger_formats.rsas aVec<String>withinHttpTriggerConfig, establishing the order of execution- The PathRouter struct in
engine/src/workers/rest_api/api_core.rsstores these IDs for runtime lookup - Execution flow in
engine/src/workers/rest_api/views.rsiterates through the array, invoking each middleware with aMiddlewareInputcontaining request metadata - Middleware must return either
action: 'continue'to proceed oraction: 'respond'to short-circuit the chain and return immediately - The engine handles channel cleanup automatically when short-circuiting occurs, preventing resource leaks
- Middleware functions can be written in any supported SDK language (TypeScript, Rust, Python) and are invoked through the standard function execution mechanism
Frequently Asked Questions
What happens if a middleware function throws an error?
If a middleware function returns an error or invalid response format, the engine treats this as a short-circuit event. According to the implementation in engine/src/workers/rest_api/views.rs, the error response is returned directly to the client, and the remaining middleware chain and handler are skipped. You should wrap middleware logic in try-catch blocks to ensure graceful error responses.
Can I modify the request object in middleware before it reaches the handler?
Currently, the middleware system in III does not support request mutation passing between chain elements. Each middleware receives the original MiddlewareInput containing path params, query params, headers, and method. While you can log, validate, or reject requests, you cannot augment the request context for downstream consumption. The handler always receives the original request data.
How does the engine handle timeouts in middleware functions?
The execute_middleware function accepts a default_timeout parameter from the REST-API configuration. This timeout applies to individual middleware invocations, meaning each function in the middleware_function_ids chain has its own deadline. If a middleware exceeds this timeout, the engine returns a timeout error response and short-circuits the chain without calling subsequent middleware or the handler.
Can middleware chains be applied to non-HTTP triggers?
No, the middleware_function_ids field is specific to HTTP triggers as defined in the HttpTriggerConfig schema. The middleware chain architecture relies on HTTP-specific constructs like headers, query parameters, and status codes that are processed in engine/src/workers/rest_api/views.rs. Other trigger types (such as scheduled or event-based triggers) do not implement this pattern in the current III codebase.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →