How to Implement Custom Middleware in OmniRoute: A Complete Guide
To implement custom middleware in OmniRoute, register a JavaScript hook via the registerHook() function in src/lib/middleware/registry.ts, which executes your code in a sandboxed Node VM with a 5-second timeout before the routing phase.
OmniRoute provides an extensible pre-request hook pipeline that allows you to intercept, mutate, or block requests before they reach provider selection and execution. This guide explains how to leverage the hook registry, types, and persistence layers to build production-ready custom middleware for the diegosouzapw/OmniRoute repository.
Understanding the Middleware Architecture
OmniRoute's middleware system centers on three interconnected components that manage the lifecycle of custom hooks.
Core Components
The hook registry (src/lib/middleware/registry.ts) acts as a singleton that stores definitions, compiles functions, tracks execution logs, and manages health statistics. It handles registration order and ensures hooks run sequentially by priority.
Strong typing is enforced through hook types (src/lib/middleware/types.ts), which define contracts for HookConfig, PreRequestHookContext, and HookResult objects. These guarantees ensure every hook receives a safe sandbox while allowing controlled mutations to specific request properties.
For durability, a persistence layer (src/lib/db/middleware.ts) provides CRUD operations against the middleware_hooks SQLite table, enabling hooks to survive restarts and be managed via the admin UI or API.
Execution Flow
When a request hits an API route like /api/v1/chat/completions, OmniRoute executes the following steps:
- Shared middleware runs first (e.g.,
requireJsonContentTypeorbodySizeGuard). - Context creation:
createHookContext()builds aPreRequestHookContextcontaining the parsed body, headers, model name, optional combo ID, and logger. - Registry execution:
runHooks(context, comboId?)loads all enabled hooks matching the request scope (global or combo-specific), sorts them bypriority, and executes each in a Node VM sandbox with a hardHOOK_EXECUTION_TIMEOUT_MSof 5000 milliseconds. - Result processing: Each hook returns a
HookResultthat may mutatebody,headers,model, orcombo, provide aresponseto short-circuit the pipeline, or setskipRemainingto halt further processing. - Post-hook routing: The potentially modified request proceeds to combo routing and execution.
Safety Guarantees
Hooks run inside a vm.Script with a deliberately restricted global object constructed by createHookSandbox() (lines 76-108 of the registry). This sandbox explicitly removes process, require, fetch, Buffer, and timers to prevent unauthorized system access. An async Promise.race guard ensures runaway code aborts after 5 seconds.
Implementing Your First Custom Hook
You can implement custom middleware in OmniRoute by writing JavaScript code that receives a mutable context object and returns a HookResult.
Basic Header Injection
To inject custom headers into every request, create a hook that modifies context.headers:
// myHeaderHook.ts
export const code = `
// Add a custom header that downstream handlers can read.
context.headers['x-my-custom'] = 'omni-route';
// No short-circuit, just mutate the request.
`;
Register the hook at startup or via the admin API:
import { registerHook } from '@/lib/middleware/registry';
import { HookPriority } from '@/lib/middleware/types';
registerHook({
name: 'addMyHeader',
description: 'Inject X-My-Custom header into every request',
priority: HookPriority.LOW,
scope: { type: 'global' },
enabled: true,
code,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
runCount: 0,
});
The registry automatically compiles the code and queues it for execution after higher-priority hooks.
Blocking Requests Conditionally
To short-circuit the pipeline and return an error response, provide a response object in your HookResult:
// blockModelHook.ts
export const code = `
const blockedModels = ['gpt-4-evil', 'claude-unstable'];
if (blockedModels.includes(context.model)) {
return {
response: {
status: 403,
body: { error: { message: 'Model blocked by policy', type: 'invalid_request_error' } },
},
};
}
`;
import { registerHook } from '@/lib/middleware/registry';
import { HookPriority } from '@/lib/middleware/types';
registerHook({
name: 'blockUnsafeModels',
description: 'Reject requests that ask for disallowed models',
priority: HookPriority.CRITICAL,
scope: { type: 'global' },
enabled: true,
code,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
runCount: 0,
});
When a hook returns a response, runHooks() immediately stops processing and returns that response to the client.
Registration Methods
OmniRoute supports multiple patterns for registering custom middleware, depending on your deployment and persistence requirements.
Runtime Registration
For testing or dynamic administration, call registerHook() directly with a complete HookConfig object. This adds the hook to the in-memory registry without database persistence.
Database Persistence
To survive restarts, persist hooks in the middleware_hooks table using SQL or the DB API (createHook, updateHook):
INSERT INTO middleware_hooks (
name, description, priority, scope_type, enabled, code,
created_at, updated_at, run_count, last_error
) VALUES (
'logRequestBody',
'Log first 200 chars of the request body for debugging',
200,
'global',
1,
'if (context.body && typeof context.body === "object") {
const snippet = JSON.stringify(context.body).slice(0,200);
context.log.info("bodyLog", `Body snippet: ${snippet}`);
}',
datetime('now'),
datetime('now'),
0,
NULL
);
After insertion, OmniRoute's loadHooksFromConfig() function automatically compiles and installs the hook on the next startup or reload cycle.
Combo-Scoped Hooks
Restrict middleware to specific provider combinations using combo-scoped hooks:
registerHook({
name: 'comboRateLimit',
description: 'Apply extra rate-limit to the "expensiveCombo"',
priority: HookPriority.HIGH,
scope: { type: 'combo', comboId: 'expensiveCombo' },
enabled: true,
code: `
// Simple in-memory counter for illustration
const key = \`combo-\${context.combo}-\${context.headers['x-request-id']}\`;
const count = Number(globalThis[key] ?? 0) + 1;
globalThis[key] = count;
if (count > 5) {
return {
response: {
status: 429,
body: { error: { message: 'Combo rate limit exceeded' } }
}
};
}
`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
runCount: 0,
});
Only requests resolving to expensiveCombo execute this hook; other traffic bypasses it entirely.
Built-in Reference Implementations
Study these built-in examples in src/shared/middleware/ to understand production patterns:
requireJsonContentType.ts: Rejects non-JSON POSTs with a 415 responsecorrelationId.ts: Creates async-local request IDs for distributed tracingbodySizeGuard.ts: Enforces per-route request size limits with 413 responses
These run before the hook pipeline in route handlers (e.g., src/app/api/v1/chat/completions/route.ts), demonstrating how shared middleware and custom hooks cooperate.
Summary
- Custom middleware in OmniRoute is implemented via sandboxed JavaScript hooks registered through
src/lib/middleware/registry.ts. - Hooks execute in a Node VM sandbox with a 5-second timeout and restricted globals (no
process,require, orfetch). - Use
registerHook()for runtime registration or themiddleware_hooksSQLite table for persistence across restarts. - Hooks can mutate
body,headers,model, andcombo, or short-circuit the pipeline by returning aresponseobject. - Scope hooks globally or to specific combo IDs to control execution breadth.
Frequently Asked Questions
What is the execution timeout for custom middleware in OmniRoute?
OmniRoute enforces a hard timeout of 5000 milliseconds (HOOK_EXECUTION_TIMEOUT_MS) for all custom hooks. The registry uses Promise.race to abort execution if code exceeds this limit, preventing runaway scripts from blocking the request pipeline.
How do I stop processing remaining hooks and return immediately?
Return a HookResult containing a response object. When runHooks() detects a response property, it immediately halts further hook execution and returns your specified status code and body to the client. You can also set skipRemaining: true to stop processing without sending a response.
Can I access external APIs or the filesystem from within a hook?
No. The sandbox constructed by createHookSandbox() in src/lib/middleware/registry.ts explicitly removes fetch, require, process, Buffer, and all timer functions. Hooks operate in a restricted environment that can only read and mutate the provided context object.
Where are custom hooks stored and how do they survive restarts?
Hooks persisted via the DB API or SQL are stored in the middleware_hooks SQLite table managed by src/lib/db/middleware.ts. On startup, loadHooksFromConfig() reads this table and automatically registers all enabled hooks, ensuring your custom middleware survives application restarts.
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 →