matrix: derive the gateway's body cap from maxRequestSize

Two limits governed a matrix upload and nothing kept them in
agreement: the documented option reached the homeserver, while the
nginx location every client request traverses carried its own
`client_max_body_size 50M`. Raising the option past 50M changed
nothing — nginx returned 413 at the proxy, naming neither matrix nor
the option that had just been raised.

The cap is not set equal to the option. Equality would regress the
default: at 20 MB a 25 MB upload is currently rejected by the
homeserver, with a matrix error a client can act on, and equality
turns that into a gateway 413. The proxy must never be the party that
rejects, so it tracks the option with headroom. This is the shape the
forge vhost already uses for git, where a generous proxy cap lets the
application own the real limit.

The module-eval arm sets a deliberately odd size so the number it
looks for cannot have come from a default, and asserts the rendered
location. Its control names the whole directive rather than the bare
old value: nginx comments render into the config, so the comment above
the directive mentions 50M and a looser arm matched itself.
This commit is contained in:
atlas 2026-09-07 15:48:40 +02:00 committed by mara
commit 2e1c15dc98
2 changed files with 33 additions and 1 deletions

View file

@ -455,6 +455,10 @@ in
Maximum size in bytes of a single matrix client request body.
Default 20 MB matches the matrix-spec recommendation for
media uploads + the upstream tuwunel default.
This is the only limit to set: the gateway's own body cap is
derived from it, with headroom, so the homeserver stays the one
that rejects an oversized upload.
'';
};
@ -612,7 +616,13 @@ in
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
client_max_body_size 50M;
# Tracks `maxRequestSize` with headroom so the homeserver stays
# the tighter limit: its rejection is a matrix error a client can
# act on, where a 413 here names neither matrix nor the option the
# operator just raised. A second literal beside it was free to
# disagree, and did — raising the option past the old 50M changed
# nothing.
client_max_body_size ${toString (deployCfg.matrix.maxRequestSize + 1048576)};
proxy_read_timeout 1h;
proxy_send_timeout 1h;
${gatewayCfg.lib.securityHeaders}

View file

@ -136,6 +136,13 @@ let
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
};
# A deliberately odd `maxRequestSize`, so the number the assertion looks for
# cannot have come from a default or from another module's literal.
matrixBodyCap = hive {
deploy.singleHostSwarm = true;
deploy.matrix.maxRequestSize = 99000000;
};
controllerOldPath = hive {
deploy.swarm-controller.enable = true;
swarm.controller.socketPath = "/run/test-ctrl/ctrl.sock";
@ -466,6 +473,21 @@ let
&& lib.hasInfix "auth_request" m.extraConfig
&& lib.hasInfix "auth_request" l.extraConfig;
}
{
# Two limits governed a matrix upload and nothing kept them in agreement,
# so raising the documented one past the gateway's hardcoded cap changed
# nothing. The second clause is the control: the old directive is gone, so
# a pass means the value is derived rather than that `hasInfix` matched
# something incidental. It targets the whole directive rather than the
# bare size, because the rendered config carries the comment above it and
# a prose mention of the old value would defeat a looser arm.
name = "the matrix gateway's body cap is derived from maxRequestSize, not a literal";
ok =
let
c = matrixBodyCap.services.nginx.virtualHosts."chat.t.local".locations."/_matrix/".extraConfig;
in
lib.hasInfix "client_max_body_size 100048576;" c && !(lib.hasInfix "client_max_body_size 50M" c);
}
{
# The arm that actually protects something. A pusher handed
# `error_page 401 =302` FOLLOWS it and POSTs its batch at a login page,