HTTP bridge
registerHttpBridgeRoute connects a catalogued fnKey to a synthetic in-process HTTP request on your Express app. When the bridge is active, executeRPC does not call originalFn directly; it sends method + path with a JSON body built from RPC args, using invokeExpressSynthetic.
This does not replace normal HTTP. Browsers and clients still call your public URLs the same way. The bridge is an extra path used when QA (or tests) invoke that fnKey over RPC or the inspector WebSocket.
Why use it
Default RPC calls the registered function reference (class method or cat() wrapper) with positional args. That skips anything that only runs inside Express: global app.use middleware, express.json(), route-level middleware, auth that reads req, and the exact req.body shape your route expects.
Reasons to opt in:
- You want invoke to prove the same code path as
POST /api/...(middleware + handler), not a shortcut into the controller. - Your handler is
(req, res)-shaped; driving it through HTTP matches production more closely than calling a separate service function. - You keep a single allowlisted
fnKeyin the catalog for QA, while still validating the HTTP response (status, headers, parsed body) returned from the route.
Benefits
- Full Express pipeline for that route: whatever is mounted on
appformethod+pathruns (including multi-handler stacks fromregisterCatPipeline). - In-process, no deploy: synthetic invoke uses a local ephemeral port; good for CI and local tooling.
- Stable RPC surface: QA still sends
fnKey+ positionalargs;mapArgsToBodyis your adapter to the JSON body your route reads. - Optional JSON Schema: registry
paramsJsonSchema/returnJsonSchemastill apply around the bridge path when configured.
How this fits with registerCatPipeline
| Piece | Role |
|---|---|
registerCatPipeline | Mounts handlers on a router, sets pipelineId / pipelineIndex, and wires inspector behavior. Defines the HTTP stack for that route. |
registerHttpBridgeRoute | Does not mount routes. It only says: for this fnKey, when executeRPC runs, issue one synthetic HTTP request to the given method + path on the given app. |
Use both when you want the route defined with registerCatPipeline and RPC invokes to hit that same URL. The bridge never receives an array of middleware; it inherits whatever Express already dispatches for that path.
mapArgsToBody
executeRPC passes args as a positional array (same arity as the registry entry’s params).
args[0]— first RPC argumentargs[1]— second argument- and so on
mapArgsToBody converts that tuple into the JSON object sent as the request body (after JSON.stringify inside synthetic invoke). Your Express route should read req.body in the same shape you return from mapArgsToBody.
Example: executeRPC({ fnKey: 'Orders.Create', args: ['user-42', 'SKU-9'] }) with
mapArgsToBody: (args) => ({ userId: args[0], sku: args[1] })
produces body { "userId": "user-42", "sku": "SKU-9" }.
Full example
Express route registered with registerCatPipeline; RPC invokes go through HTTP via the bridge.
import express from 'express'
import {
cat,
registerCatPipeline,
registerHttpBridgeRoute,
executeRPC,
resetInspectorState,
} from '@gloocan/cat-inspector'
resetInspectorState()
const app = express()
app.use(express.json())
// Handler reads req.body — same shape mapArgsToBody produces
const createOrder = cat(
'Orders.Create',
(req: express.Request, res: express.Response) => {
const { userId, sku } = req.body as { userId?: string; sku?: string }
res.status(201).json({ ok: true, userId, sku })
},
)
const router = express.Router()
registerCatPipeline(router, 'post', '/api/orders', [createOrder])
app.use(router)
// RPC uses positional args: [ userId, sku ] → body { userId, sku }
registerHttpBridgeRoute(app, {
fnKey: 'Orders.Create',
method: 'post',
path: '/api/orders',
mapArgsToBody: (args) => ({
userId: args[0], // first RPC argument
sku: args[1], // second RPC argument
}),
})
// Later, from QA / tests:
const resp = await executeRPC({
requestId: 'r1',
fnKey: 'Orders.Create',
args: ['user-42', 'SKU-9'],
})
// On success, result includes HTTP metadata from the bridge path, e.g. { http: { statusCode, headers, body } }For a pipeline of middleware plus controller, list every handler in registerCatPipeline as usual; a single registerHttpBridgeRoute to that same path still runs all of them, because Express invokes the full stack for one request.
registerHttpBridgeRoute
function registerHttpBridgeRoute(
app: Express,
spec: {
fnKey: string
method: HttpMethod
path: string
mapArgsToBody: (args: unknown[]) => unknown
},
): voidPreconditions
spec.fnKeymust already exist in the registry (registered viacat,catModule, or@Cat).- Calling registration sets the entry’s
invokeKindto'http_synthetic'.
Effect
executeRPC will no longer call originalFn directly for that key. Instead it serializes args through mapArgsToBody and runs an internal invokeExpressSynthetic against app.
clearHttpBridgeRegistry
Removes all bridge specs. Useful in tests (see testing-resets).
HttpBridgeSpec
type HttpBridgeSpec = {
app: Express
method: HttpMethod
path: string
mapArgsToBody: (args: unknown[]) => unknown
}This type describes the stored configuration; hosts normally use registerHttpBridgeRoute rather than constructing specs manually.
Errors
- Unknown
fnKey→ throws at registration time. - Duplicate registration for the same
fnKey→ throws. - At invoke time, misconfigured bridges yield
HTTP_BRIDGE_NOT_CONFIGURED-styleRpcResponseerrors when the spec is missing.