Server

Headers

Read and send HTTP headers on the server and in the client.

HeadersSubset is the type for the headers a handler reads or sends. mion validates them like any other param.

Reading Request Headers

mion.headersMiddleware creates a middleware that reads request headers. Its second parameter is a HeadersSubset<Required, Optional>: the first type lists the headers a request must send, the second those it may send.

import {createMionRouter} from '@mionjs/router';
import {HeadersSubset} from '@mionjs/core';

const mion = createMionRouter();

const traceWithOptionalAgent = mion.headersMiddleware(
  async (
    ctx,
    {headers}: HeadersSubset<'X-Trace-Id', 'User-Agent'>
  ): Promise<void> => {
    const traceId = headers['X-Trace-Id']; // always present
    const userAgent = headers['User-Agent']; // may be undefined

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

export {traceWithOptionalAgent};
RuleWhat it means
Name caseNot case sensitive: a request with authorization matches Authorization. You read the value by the name you declared.
ValuesAlways strings. Parse numbers or dates yourself.
Other paramsParams after the HeadersSubset come from the request body, as in any middleware.
Only the declared names reach the handler. To read any other header, use ctx.request.headers.get(name), see Call Context.

Missing or Invalid Headers

A request without a required header, or with an empty one, ends with a validation-error (HTTP 422) before the handler runs. The client gets it typed like any validation error.

Sending Response Headers

Return a HeadersSubset from a middleware or a route to send its values as response headers, never in the body. In code built with mion, new HeadersSubset(...) throws a headers-validation-error inside your handler when a header is missing or has the wrong type, and no header is sent. The router does not check headers after your handler returns.

import {HeadersSubset} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';

const mion = createMionRouter();

const routes = {
  // sets the header on the response of every route after it
  noCache: mion.middleware(
    (ctx): HeadersSubset<'Cache-Control'> =>
      new HeadersSubset({'Cache-Control': 'no-store'})
  ),
  // reads a request header and returns a response header
  locale: mion.headersMiddleware(
    (
      ctx,
      {headers}: HeadersSubset<never, 'Accept-Language'>
    ): HeadersSubset<'Content-Language'> =>
      new HeadersSubset({
        'Content-Language': headers['Accept-Language'] ?? 'en',
      })
  ),
} satisfies Routes;
An optional header you leave out is not sent. When two handlers set the same header, the last one in the execution chain wins.

Building Headers in a Generic Function

Inside a generic function mion does not know the header names, so the build stops with marker-in-generic-function. Pass undefined as the second argument to skip the check.

import {HeadersSubset} from '@mionjs/core';

function withHeader<Name extends string>(
  headers: Record<Name, string>
): HeadersSubset<Name> {
  return new HeadersSubset<Name>(headers, undefined);
}

export {withHeader};

Headers on Every Response

The responseHeaders router option adds headers to every response, on every platform. When two sources set the same header, the one higher in the table wins.

import {createMionRouter} from '@mionjs/router';

// also sent on error responses
createMionRouter({responseHeaders: {'x-api': 'store'}});
OrderSource
1A handler: a returned HeadersSubset or ctx.response.headers
2The router's responseHeaders
3server: mionjs, added by mion
Every response has content-type: application/json; charset=utf-8. Neither a handler nor responseHeaders can change it.

Build Version Header

Only routes that run the mionFetchMetadata middleware send an x-build-version header: the version of the API the server was built from. A client built against another version gets an api-version-mismatch error, see API Version Check. Set apiVersionCheck: false to stop sending the header and turn the client check off.

A version set in responseHeaders replaces it on every response.

const mion = createMionRouter({apiVersionCheck: false});

Error Type Header

When an error ends the request, the response gets an x-rpc-error header with the error's type. A proxy or a log can read it without parsing the body.

Only the first error that ends the request sets it. A type with characters other than letters, digits and _ . : @ -, or longer than 128 characters, makes the header read unknown-error. The body still has the full type.

Sending Headers from the Client

On the client, send a headers middleware's HeadersSubset from its onRequest hook. For fixed headers on every request, use fetchOptions.headers in initClient.

client.ts
import {HeadersSubset} from '@mionjs/core';
import {initClient} from '@mionjs/client';
import type {HeadersApi} from '../router/headers-api.routes.ts';

const {routes, middlewares} = initClient<HeadersApi>({
  baseURL: 'http://localhost:3000',
  fetchOptions: {headers: {'Accept-Language': 'en'}},
});

// the HeadersSubset is sent as HTTP headers, not in the body
middlewares.trace.onRequest((trace) =>
  trace(new HeadersSubset({'X-Trace-Id': crypto.randomUUID()}))
);

await routes.getDownloadUrl('report-42').call();
server.routes.ts
import {HeadersSubset} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';

const mion = createMionRouter();

const routes = {
  trace: mion.headersMiddleware(
    (ctx, {headers}: HeadersSubset<'X-Trace-Id'>): void => {
      console.log('trace', headers['X-Trace-Id']);
    }
  ),
  // returns a header, not a body value
  getDownloadUrl: mion.route(
    (ctx, fileId: string): HeadersSubset<'x-download-url'> =>
      new HeadersSubset({'x-download-url': `/files/${fileId}`})
  ),
} satisfies Routes;

const api = mion.initRoutes(routes);

export type HeadersApi = typeof api;
When both set the same header, the HeadersSubset value wins. More on hooks in Middleware Hooks.

Reading Response Headers on the Client

When a route returns a HeadersSubset, call() returns a HeadersSubset built from the response headers. The client reads only the names the return type declares.

client.ts
import {initClient} from '@mionjs/client';
import type {HeadersApi} from '../router/headers-api.routes.ts';

const {routes} = initClient<HeadersApi>({baseURL: 'http://localhost:3000'});

const [download] = await routes.getDownloadUrl('report-42').call();

if (download) console.log(download.headers['x-download-url']);
server.routes.ts
import {HeadersSubset} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';

const mion = createMionRouter();

const routes = {
  trace: mion.headersMiddleware(
    (ctx, {headers}: HeadersSubset<'X-Trace-Id'>): void => {
      console.log('trace', headers['X-Trace-Id']);
    }
  ),
  // returns a header, not a body value
  getDownloadUrl: mion.route(
    (ctx, fileId: string): HeadersSubset<'x-download-url'> =>
      new HeadersSubset({'x-download-url': `/files/${fileId}`})
  ),
} satisfies Routes;

const api = mion.initRoutes(routes);

export type HeadersApi = typeof api;
A browser calling a server on another origin hides every custom response header, x-build-version included. List the ones the client reads in Access-Control-Expose-Headers, for example with responseHeaders.
Copyright © 2026