route-adapter: write a body limit as the predecessor's buffering middleware

The adapter skips what its one file shape cannot say. A body limit is the exception: the predecessor
has a buffering middleware and served its own registry name with exactly it, so this is written
rather than skipped, named after the router so the two halves cannot drift.

A limit that is not a whole positive number of bytes takes the route with it. Written without the
limit, the predecessor would carry what the module said not to carry and this module would report
success. Silence stays silence — no middleware, the predecessor's default.
This commit is contained in:
jochen
2026-09-26 16:01:23 +02:00
parent 3d73c9f54e
commit b2e39eb2cd
3 changed files with 105 additions and 3 deletions
+51 -1
View File
@@ -75,6 +75,15 @@ export interface Route {
from: string;
/** Where the predecessor's proxy is to send it. */
target: string;
/**
* The largest request body, in bytes, the predecessor may carry to it — the contribution's
* `max-request-body`. Absent is whatever the predecessor does by default.
*
* Unlike a policy, this file shape *can* say it: the predecessor has a buffering middleware, and
* its own registry route used exactly this. A registry takes image layers in single requests of
* gigabytes, so a route that could not say it would be a name nothing could be pushed to.
*/
maxRequestBody?: number;
}
/** What one pass changed. */
@@ -176,12 +185,40 @@ export function routesFrom(document: unknown, machine: string): { routes: Route[
}
// Where the mesh says that machine is. Empty means this one, and this one is reached from
// inside the predecessor's container by the machine's own name, not by loopback.
// A limit it cannot honour is a route it does not write — skipped and named, like a port that
// is not one. Written without the limit instead, the predecessor would carry exactly what the
// module said not to carry, and this adapter would report success.
const askedLimit = entry.values?.["max-request-body"];
const limit = asBodyLimit(askedLimit);
if (limit === null) {
skipped.push(
`${from} asked for route ${name} with a max-request-body of ${JSON.stringify(askedLimit)}, ` +
`which is not a whole positive number of bytes`,
);
continue;
}
const at = typeof entry.at === "string" && entry.at.trim() !== "" ? entry.at.trim() : machine;
routes.push({ name, from, target: `http://${at}:${port}` });
routes.push({ name, from, target: `http://${at}:${port}`, ...(limit === undefined ? {} : { maxRequestBody: limit }) });
}
return { routes, skipped };
}
/**
* The body limit a contribution asked for: a number, `undefined` for silence, `null` for unusable.
*
* Three answers rather than two, because "said nothing" and "said something wrong" must not become
* the same route.
*/
function asBodyLimit(value: unknown): number | undefined | null {
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
return null;
}
return value;
}
/** The file one route is written to. The prefix is how the mesh recognises its own. */
export function fileNameFor(name: string): string {
return `mesh-${name}.yml`;
@@ -201,6 +238,10 @@ export function routerNameFor(name: string): string {
*/
export function routeFile(route: Route, settings: Settings): string {
const id = routerNameFor(route.name);
// The body limit is a middleware in the predecessor's vocabulary — its `buffering`, with the one
// field the predecessor's own registry route set — named after the router so the two halves cannot
// drift, and written only when the contribution asked for it.
const limited = route.maxRequestBody !== undefined;
return [
marker,
`# ${route.from} contributed this route. It is removed when that contribution goes.`,
@@ -210,10 +251,19 @@ export function routeFile(route: Route, settings: Settings): string {
` entryPoints: [${settings.entrypoint}]`,
` rule: Host(\`${route.name}\`)`,
` service: ${id}`,
...(limited ? [` middlewares: [${id}-body]`] : []),
" tls:",
` certResolver: ${settings.resolver}`,
" domains:",
` - main: ${route.name}`,
...(limited
? [
" middlewares:",
` ${id}-body:`,
" buffering:",
` maxRequestBodyBytes: ${route.maxRequestBody}`,
]
: []),
" services:",
` ${id}:`,
" loadBalancer:",