Server

Middleware

Learn how to define and use middleFns (middleware) in mion APIs.

Middleware functions are defined using middleFn and can be executed before or after a route. middleware are functions that can be chained to create the full Execution Chain for a route.

middleFn are useful when a route might require some extra data like authorization, filters or some extra processing like body parsing, logging, etc...

The first parameter is always the Call Context, the rest of parameters are remote parameters that get deserialized and validated before the middleFn gets executed.

Defining a Middleware function

import {CallContext, middleFn, Routes} from '@mionkit/router';
import {myApp} from './full-example.app.ts';

const routes = {
    // using the middleFn function to define a middleFn
    logger: middleFn(
        async (ctx: CallContext): Promise<void> => {
            const hasErrors = ctx.request.thrownErrors && Object.keys(ctx.request.thrownErrors).length > 0;
            if (hasErrors) await myApp.cloudLogs.error(ctx.path, ctx.request.thrownErrors);
            else myApp.cloudLogs.log(ctx.path, ctx.shared.me.name);
        },
        // ensures logger is executed even if there are errors in the route or other middleFns
        {runOnError: true}
    ),
    // ... other routes and middleFns
} satisfies Routes;

Headers Functions

For cases where we need to send or receive data in HTTP headers we use headersFn and HeadersSubset<T>. Unlike route/middleFn parameters (which are serialized to the request/response body), HeadersSubset values are serialized directly to HTTP headers.

Reading Request Headers

Use headersFn with a HeadersSubset<T> parameter to read headers from the incoming request:

import {HeadersSubset, RpcError} from '@mionkit/core';
import {headersFn, middleFn, Routes} from '@mionkit/router';
import {getAuthUser, isAuthorized} from 'MyAuth';

const routes = {
    // using the headersFn to declare request headers, headers param must be next after context
    auth: headersFn(async (ctx, {headers}: HeadersSubset<'Authorization'>): Promise<void | RpcError<'not-authorized'>> => {
        const token = headers.Authorization;
        const me = await getAuthUser(token);
        if (!isAuthorized(me)) {
            return new RpcError({type: 'not-authorized', publicMessage: 'User is not authorized'});
        }
        ctx.shared.auth = {me}; // user is added to ctx to shared with other routes/middleFns
    }),
    // set response headers
    serverName: middleFn((ctx): HeadersSubset<'Server'> => {
        return new HeadersSubset({Server: 'my-server'});
    }),
    // ... other routes and middleFns
} satisfies Routes;
Header names are NOT case sensitive. Authorization, AUTHORIZATION, and authorization all match the same header.

Setting Response Headers

Return a HeadersSubset from any middleFn to set headers in the HTTP response. This works with both middleFn and headersFn:

import {HeadersSubset, RpcError} from '@mionkit/core';
import {headersFn, middleFn, Routes} from '@mionkit/router';
import {getAuthUser, isAuthorized} from 'MyAuth';

const routes = {
    // using the headersFn to declare request headers, headers param must be next after context
    auth: headersFn(async (ctx, {headers}: HeadersSubset<'Authorization'>): Promise<void | RpcError<'not-authorized'>> => {
        const token = headers.Authorization;
        const me = await getAuthUser(token);
        if (!isAuthorized(me)) {
            return new RpcError({type: 'not-authorized', publicMessage: 'User is not authorized'});
        }
        ctx.shared.auth = {me}; // user is added to ctx to shared with other routes/middleFns
    }),
    // set response headers
    serverName: middleFn((ctx): HeadersSubset<'Server'> => {
        return new HeadersSubset({Server: 'my-server'});
    }),
    // ... other routes and middleFns
} satisfies Routes;

Optional Headers

Use two type parameters to declare required and optional headers:

import {HeadersSubset} from '@mionkit/core';
import {headersFn} from '@mionkit/router';

// HeadersSubset<RequiredHeaders, OptionalHeaders>
// - First type parameter: required headers (must be present)
// - Second type parameter: optional headers (may or may not be present)

// Example: Authorization is required, User-Agent is optional
const authWithOptionalAgent = headersFn(async (ctx, {headers}: HeadersSubset<'Authorization', 'User-Agent'>): Promise<void> => {
    // headers.Authorization is guaranteed to exist (required)
    const token = headers.Authorization;

    // headers['User-Agent'] may be undefined (optional)
    const userAgent = headers['User-Agent'];

    console.log(`Token: ${token}, Agent: ${userAgent ?? 'unknown'}`);
});

// Multiple required and optional headers
const multiHeadersFn = headersFn(
    async (
        ctx,
        {headers}: HeadersSubset<'Authorization' | 'Content-Type', 'X-Request-Id' | 'X-Correlation-Id'>
    ): Promise<void> => {
        // Required headers - always present
        const auth = headers.Authorization;
        const contentType = headers['Content-Type'];

        // Optional headers - may be undefined
        const requestId = headers['X-Request-Id'];
        const correlationId = headers['X-Correlation-Id'];

        console.log(`Auth: ${auth}, ContentType: ${contentType}`);
        console.log(`RequestId: ${requestId ?? 'none'}, CorrelationId: ${correlationId ?? 'none'}`);
    }
);

export {authWithOptionalAgent, multiHeadersFn};
Header values are limited to string types only. Complex data types cannot be serialized to HTTP headers.

Raw Middleware functions

In case we need to access the raw or native underlying request and response, we must use a rawMiddleFn.

These are functions that receive the CallContext, RawRequest, RawResponse and RouterOptions, but can't receive any remote parameters or return any data. Raw Middleware functions can only modify the CallContext and return or throw errors.

Raw Middleware functions are useful to extend the router's core functionality, i.e: The router internally uses them for serialization/deserialization.

import {rawMiddleFn, Routes} from '@mionkit/router';
import {IncomingMessage, ServerResponse} from 'http';
type HttpRequest = IncomingMessage & {body: any};

const routes = {
    // using the rawMiddleFn function to define a middleFn
    progress: rawMiddleFn(async (ctx, rawRequest: HttpRequest, rawResponse: ServerResponse): Promise<void> => {
        return new Promise((resolve) => {
            const maxTime = 1000;
            const increment = 10;
            let total = 0;
            const intervale = setInterval(() => {
                if (total >= maxTime) {
                    clearInterval(intervale);
                    resolve();
                }
                total += increment;
                rawResponse.write(`\n${total}%`);
            }, increment);
        });
    }),
    // ... other routes and middleFns
} satisfies Routes;

Force Run On Errors

When there is an error in a route or middleFn the rest of middleFns are not executed unless runOnError is set to true. This is useful for some middleFns like a logger that needs to be executed after any other middleFn and log all the errors or request data.

import {CallContext, middleFn, Routes} from '@mionkit/router';
import {myApp} from './full-example.app.ts';

const routes = {
    // using the middleFn function to define a middleFn
    logger: middleFn(
        async (ctx: CallContext): Promise<void> => {
            const hasErrors = ctx.request.thrownErrors && Object.keys(ctx.request.thrownErrors).length > 0;
            if (hasErrors) await myApp.cloudLogs.error(ctx.path, ctx.request.thrownErrors);
            else myApp.cloudLogs.log(ctx.path, ctx.shared.me.name);
        },
        // ensures logger is executed even if there are errors in the route or other middleFns
        {runOnError: true}
    ),
    // ... other routes and middleFns
} satisfies Routes;

Execution Chain

The Execution Chain is a list of all the middleFns and the route that are executed during a route call. That is: all middleFns before the route, the route itself, and all middleFns after the route. The Execution chain is generated when the the router is started so it is the same for all requests.

For every incoming request the Execution Chain is retrieved and all middleFns and the route get executed in order, there are no paths or conditionals everything is executed unless an error occurs.

Execution Order

The order in which Routes and Middleware functions are added to the router is important. Middleware functions and Routes are executed in the same order they are defined (Top Down order).
import {Routes, initMionRouter, middleFn, route} from '@mionkit/router';

const routes = {
    authorizationMiddleFn: middleFn((): void => undefined), // middleFn
    users: {
        userOnlyMiddleFn: middleFn((): void => undefined), // scoped middleFn
        getUser: route((): null => null), // route
        setUser: route((): null => null), // route
    },
    pets: {
        getPet: route((): null => null), // route
        setPet: route((): null => null), // route
    },
    errorHandlerMiddleFn: middleFn((): void => undefined), // middleFn
    loggingMiddleFn: middleFn((): void => undefined), // middleFn
} satisfies Routes;

export const myValidApi = await initMionRouter(routes);

Generated Execution Chain for: pets.getPet

graph LR A(authorizationMiddleFn) --> B{{getPet}} --> E(errorHandlerMiddleFn) --> C(loggingMiddleFn) style B color:#00dc82

Middleware function's Scope

We can limit middleFns to be executed only on a subset of routes.

The userOnlyMiddleFn from previous example will be executed only for the routes under users but not for routes under pets.

Generated Execution Chain for: users.getUser

graph LR A(authorizationMiddleFn) --> B(userOnlyMiddleFn) --> C{{getUser}} --> E(errorHandlerMiddleFn) --> D(loggingMiddleFn) style B color:#f59e0b style C color:#00dc82