Skip to main content

Goal

Run one or more middleware functions before an HTTP handler executes. Middleware can inspect the request and either continue to the handler or short-circuit with a response.

How It Works

Middleware in iii is just a regular function. The engine calls it before the handler. The function returns either { action: "continue" } to proceed, or { action: "respond", response: {...} } to short-circuit. There are two ways to attach middleware:
TypeWhere to configureScopeRuns
Per-routeTrigger config (middleware_function_ids)Specific endpointAfter condition check
Globaliii-config.yaml (rest_api.middleware)All HTTP endpointsBefore condition check

Per-Route Middleware

1. Register the middleware function

Middleware functions receive a request object with path_params, query_params, headers, and method (no body).
auth-middleware.ts
import { registerWorker } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

// Middleware: check for API key
iii.registerFunction({ id: 'middleware::require-api-key' }, async (req) => {
  const apiKey = req.request?.headers?.['x-api-key']
  if (!apiKey || apiKey !== process.env.API_KEY) {
    return {
      action: 'respond',
      response: {
        status_code: 401,
        body: { error: 'Invalid or missing API key' },
      },
    }
  }
  return { action: 'continue' }
})

// Handler: only runs if middleware continues
iii.registerFunction({ id: 'api::secret-data' }, async (req) => ({
  status_code: 200,
  body: { secret: 'the answer is 42' },
}))

2. Attach middleware to the trigger

Include middleware_function_ids in the trigger config. Middleware runs in the order listed.
auth-middleware.ts (continued)
iii.registerTrigger({
  type: 'http',
  function_id: 'api::secret-data',
  config: {
    api_path: 'secret',
    http_method: 'GET',
    middleware_function_ids: ['middleware::require-api-key'],
  },
})

3. Test it

# Without API key: 401
curl http://localhost:3111/secret
# {"error":"Invalid or missing API key"}

# With API key: 200
curl -H "x-api-key: my-secret-key" http://localhost:3111/secret
# {"secret":"the answer is 42"}

Chaining Multiple Middleware

List multiple function IDs. They execute in order. If any short-circuits, the rest are skipped.
iii.registerTrigger({
  type: 'http',
  function_id: 'api::admin-dashboard',
  config: {
    api_path: 'admin/dashboard',
    http_method: 'GET',
    middleware_function_ids: [
      'middleware::request-logger',    // runs first
      'middleware::require-api-key',   // runs second (if first continues)
      'middleware::require-admin-role', // runs third (if second continues)
    ],
  },
})

Global Middleware

Global middleware runs on every HTTP request, before route-level conditions and per-route middleware. Configure it in iii-config.yaml:
iii-config.yaml
- class: modules::api::RestApiModule
  config:
    port: 3111
    middleware:
      - function_id: "global::rate-limiter"
        phase: preHandler
        priority: 5           # lower number = runs first
      - function_id: "global::request-logger"
        phase: preHandler
        priority: 10
Register the global middleware functions in a worker, just like any other function:
iii.registerFunction({ id: 'global::rate-limiter' }, async (req) => {
  // rate limiting logic...
  return { action: 'continue' }
})

iii.registerFunction({ id: 'global::request-logger' }, async (req) => {
  console.log(`${req.request.method} ${JSON.stringify(req.request.path_params)}`)
  return { action: 'continue' }
})

Middleware Response Protocol

Every middleware function must return one of:
// Continue: pass control to the next middleware or handler
{ action: "continue" }

// Short-circuit: return a response immediately, skip remaining middleware and handler
{
  action: "respond",
  response: {
    status_code: 403,
    body: { error: "Forbidden" },
    headers: { "X-Rejected-By": "auth-middleware" }  // optional
  }
}

Middleware Input

Middleware receives a lightweight request object (no body, for performance):
{
  phase: "preHandler",
  request: {
    path_params: { id: "123" },
    query_params: { page: "1" },
    headers: { authorization: "Bearer ...", "content-type": "application/json" },
    method: "GET"
  },
  context: {}
}
Middleware does not receive the request body. This is intentional: global middleware runs before body parsing, so auth checks and rate limiting skip the expensive JSON parse for rejected requests. Use conditions for body-based validation.

Request Lifecycle

  Request arrives


  Route match


  Global middleware (from config, sorted by priority)
       │ ── short-circuit? ──▶ Return response

  Condition check (if configured)
       │ ── fails? ──▶ Return 422

  Per-route middleware (from trigger config, in order)
       │ ── short-circuit? ──▶ Return response

  Body parsing


  Handler function


  Return response

Error Handling

ScenarioEngine behavior
Middleware returns { action: "continue" }Proceeds to next middleware or handler
Middleware returns { action: "respond", response }Returns the response, skips handler
Middleware returns invalid actionLogs warning, treats as continue
Middleware returns no resultLogs warning, treats as continue
Middleware throws an errorReturns 500 with error ID for debugging
Middleware exceeds timeoutReturns 504 Gateway Timeout