OCPP WS IOocpp-ws-io
Core WebSocket RPC

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 uses a discriminated union based on the type property. You can intercept all 4 phases of an RPC message:

PropertyTypeDescription
type"incoming_call" | "outgoing_call" | "incoming_result" | "outgoing_result" | "incoming_error" | "outgoing_error"Phase and direction of the message
methodstringOCPP Action (e.g. BootNotification)
messageIdstringUnique ID of the message
paramsany (only on calls)Payload of the CALL
payloadany (only on results)Payload of the CALLRESULT
errorOCPPCallError (only on errors)Payload of the CALLERROR
stateRecord<string, any>Custom user data passed down the chain
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:

  1. Downstream (Request Phase): Code before await next() executes in the order middleware was registered (use(a) then use(b)).
  2. Upstream (Response Phase): Code after await next() executes in reverse order (from b back to a).

If a middleware does not call await next(), the chain is short-circuited entirely.

On this page