Middleware
Intercept and modify OCPP messages.
Middleware
The middleware system allows you to intercept, inspect, and modify OCPP messages (calls and results) as they flow through the client and server. It follows an onion-like execution model similar to Koa or Axios interceptors.
Basic Usage
You can add middleware to both OCPPServer and OCPPClient.
client.use(async (ctx, next) => {
console.log(`Processing ${ctx.method} (${ctx.direction})`);
// Modify context or just observe
ctx.params.timestamp = new Date().toISOString();
// Proceed to next middleware/handler
await next();
// Code here runs after the handler returns (on the way out)
console.log(`Finished ${ctx.method}`);
});Built-in Middleware
Logging
The logging middleware is enabled by default but can be customized. It logs all incoming and outgoing messages.
import { createLoggingMiddleware } from "ocpp-ws-io/middleware";
// Manually adding (if you disabled default logging)
client.use(createLoggingMiddleware(logger, "Point1"));Creating Custom Middleware
Middleware functions receive a context and a next function.
Context Object
The ctx object is a discriminated union on the type property. All six phases of an
RPC exchange run the chain — a request and its response in both directions, plus the
two error phases. Every phase carries messageId, method and the fields below:
ctx.type | Carries | Runs when |
|---|---|---|
incoming_call | params, protocol? | A CALL arrives, before your handler sees it |
outgoing_call | params, options | You send a CALL, before it is written |
incoming_result | payload | A CALLRESULT answers a call you made |
outgoing_result | payload | Your handler returned, before the CALLRESULT is written |
incoming_error | error (OCPPCallError) | A CALLERROR answers a call you made |
outgoing_error | errorCode, errorDescription | Your handler threw, before the CALLERROR is written |
method on the two incoming response phases is correlated from the pending call — the
wire message does not carry it. The same is true outbound: a CALLRESULT is
[3, messageId, payload] and a CALLERROR is [4, id, code, description, details], so
outgoing_result and outgoing_error are the only place to make a decision that
depends on which request is being answered.
Middleware transforms; plugin hooks observe and veto. Assigning to ctx.params,
ctx.payload, ctx.errorCode or ctx.errorDescription changes the message that is
actually sent or handled. Plugin hooks such as onBeforeSend and onBeforeReceive
can allow or block a message but cannot rewrite one. Reach for a hook to gate, and
middleware to change.
On the server, a request and its response nest — the response chain runs inside the request's, so a middleware written in the wrapping style sees its own exchange close last:
in:incoming_call → in:outgoing_result → out:outgoing_result → out:incoming_callIf a middleware throws while a response is being built, the library fails open: it sends what the handler produced and logs the failure. A broken middleware must never leave a charge point waiting for a CALLRESULT that never arrives.
import { defineRpcMiddleware } from "ocpp-ws-io/browser"; // Or "ocpp-ws-io" for Server/NodeClient
const validationMiddleware = defineRpcMiddleware(async (ctx, next) => {
if (ctx.type === "outgoing_call" && !ctx.params) {
throw new Error("Payload cannot be empty");
}
// You can wrap the inner execution in a try/catch
try {
const result = await next();
return result;
} catch (error) {
console.error(`Handler crashed during ${ctx.method}`, error);
throw error;
}
});
client.use(validationMiddleware);Connection Middleware (Server Setup)
While clients only utilize RPC middleware (parsing payloads), the OCPPServer and OCPPRouter handle raw HTTP upgrades. To gain type-safety when building connection middlewares (e.g. rate-limiting, authentication), use defineMiddleware and the native ctx controls:
import { defineMiddleware } from "ocpp-ws-io";
const rateLimitConnection = defineMiddleware(async (ctx) => {
// `ctx` includes full `IncomingMessage`, URL parsed `pathname`, and `headers`
console.log(`Connection attempt from: ${ctx.handshake.remoteAddress}`);
if (isRateLimited(ctx.handshake.remoteAddress)) {
// Instantly aborts the WebSocket connection with an HTTP code
ctx.reject(429, "Too Many Requests");
} else {
// Or proceed down the execution chain. You can optionally pass an object
// to next(), which will automatically be shallow-merged into `ctx.state`.
await ctx.next({
isTrusted: true,
rateLimitRemaining: 99,
});
}
});
server.use(rateLimitConnection);Authentication Helpers
To secure incoming WebSocket connections on the OCPPServer, you attach an auth() hook. ocpp-ws-io provides two utilities to make authentication chains typed and composable.
defineAuth
The defineAuth helper provides immediate IDE type inference for the accept, reject, and handshake parameters, so you don't need to manually type your callback.
import { defineAuth } from "ocpp-ws-io";
const verifyBasicAuth = defineAuth(async (ctx) => {
const token = ctx.handshake.headers.authorization;
if (!token) {
return ctx.reject(401, "Basic auth required");
}
// Inject arbitrary session properties that your RPC handlers can access later
ctx.accept({ session: { identity: ctx.handshake.identity, role: "admin" } });
});
server.auth(verifyBasicAuth);combineAuth
When building modular endpoints with the OCPPRouter, you might need to combine multiple authentication rules (e.g. check a firewall IP, then check a JWT). combineAuth executes defineAuth callbacks sequentially.
import { combineAuth, defineAuth } from "ocpp-ws-io";
const checkFirewall = defineAuth((ctx) => {
if (ctx.handshake.remoteAddress === "1.2.3.4") ctx.reject(403, "IP Blocked");
else ctx.accept({}); // Pass immediately to the next auth handler
});
server.auth(combineAuth(checkFirewall, verifyBasicAuth));
// Alternatively, inline:
server.auth(
combineAuth(
// Firewall Check
async (ctx) => {
if (isBlocked(ctx.handshake.headers["x-forwarded-for"])) {
return ctx.reject(403, "IP Blocked");
}
},
// Basic Auth
async (ctx) => {
if (!ctx.handshake.headers.authorization) {
return ctx.reject(401, "Missing Auth");
}
ctx.accept({ protocol: "ocpp1.6" });
},
),
);Execution Flow
Because ocpp-ws-io uses an onion model (await next()), execution happens in two distinct phases:
- Downstream (Request Phase): Code before
await next()executes in the order middleware was registered (use(a)thenuse(b)). - Upstream (Response Phase): Code after
await next()executes in reverse order (frombback toa).
If a middleware does not call await next(), the chain is short-circuited entirely.