# How to Implement Middleware Chains for HTTP Triggers Using `middleware_function_ids` in III

> Streamline III HTTP triggers by chaining middleware using middleware_function_ids. Learn how to control request flow with return actions for continue or respond.

- Repository: [iii/iii](https://github.com/iii-hq/iii)
- Tags: how-to-guide
- Published: 2026-05-28

---

**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:

```typescript
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`](https://github.com/iii-hq/iii/blob/main/engine/src/trigger_formats.rs). The `HttpTriggerConfig` struct explicitly includes the **middleware chain** definition:

```rust
// 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`](https://github.com/iii-hq/iii/blob/main/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`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/rest_api/api_core.rs) (lines 39-46):

```rust
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:

1. **Global middleware** execution from REST-API configuration
2. **Per-route middleware** iteration over `middleware_function_ids`
3. **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`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/rest_api/views.rs) (lines 504-526):

```rust
// 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:

```typescript
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:

```rust
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:
1. Immediately halts middleware iteration
2. Cleans up request/response channels via `channel_mgr.remove_channel()`
3. 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`](https://github.com/iii-hq/iii/blob/main/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:

```typescript
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_ids`** is defined in [`engine/src/trigger_formats.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/trigger_formats.rs) as a `Vec<String>` within `HttpTriggerConfig`, establishing the order of execution
- The **PathRouter** struct in [`engine/src/workers/rest_api/api_core.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/rest_api/api_core.rs) stores these IDs for runtime lookup
- **Execution flow** in [`engine/src/workers/rest_api/views.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/workers/rest_api/views.rs) iterates through the array, invoking each middleware with a `MiddlewareInput` containing request metadata
- Middleware must return either **`action: 'continue'`** to proceed or **`action: '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`](https://github.com/iii-hq/iii/blob/main/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`](https://github.com/iii-hq/iii/blob/main/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.