Skip to content

Only offer expiry options the instance accepts - #139

Merged
louisbels merged 2 commits into
mainfrom
fix/front-respects-server-limits
Jul 26, 2026
Merged

Only offer expiry options the instance accepts#139
louisbels merged 2 commits into
mainfrom
fix/front-respects-server-limits

Conversation

@louisbels

Copy link
Copy Markdown
Member

Setting MAX_EXPIRY on a real deployment broke note creation. Found by deploying, not by reading.

The bug

The API enforces MAX_EXPIRY per request since the hardening work. The picker kept listing every option regardless, so on an instance capped at 7 days, choosing "30 days" failed with:

Maximum expiry is 604800 seconds

Raw, in English, whatever the user's locale. Any operator tightening retention hits this — and it is a natural thing to configure.

The root cause is that the front had no idea what the server would accept: it builds its config from the environment and never knew MAX_EXPIRY existed. It was in docker-compose.yml, applied by the API, and invisible to the UI.

The fix

MAX_EXPIRY flows into ServerConfig, the picker lists only what fits, and the default lands on the longest allowed option rather than a hardcoded 24h a tighter instance would reject. A selection carried over from a looser config snaps back into range.

Verified in a browser against MAX_EXPIRY=3600:

{"selected":"3600","options":["300=5 minutes","3600=1 heure"]}

…and creating a note then succeeds, where it previously 400'd.

Two bugs found while testing this

Size validation could silently switch off. MAX_FILE_SIZE=20MB produced NaN, and file.size > NaN is always false — so no file was ever rejected. MAX_FILE_SIZE= produced 0, rejecting every non-empty file instead. Parsing now falls back on anything unusable.

The parsing moved into $lib/server-config.ts, a pure module the coverage gate measures. It previously lived in the route loader, which is excluded from the gate — that exclusion is exactly why this went unnoticed.

A blank value aborted API startup. FOO= in a .env, or an unset ${FOO} in a compose file, arrives as "", and Number("") is 0 — which failed the positive-integer checks and stopped the server with a message that never mentioned the blank. Blank now means "use the default" for every numeric setting; a value that is present but invalid is still rejected.

I hit this one by adding the pass-through to docker-compose.dev.yml and watching the API refuse to boot.

Testing locally

docker-compose.dev.yml now forwards MAX_EXPIRY, MAX_FILE_SIZE and MAX_FILES_PER_NOTE, so a tightened instance is one command away:

MAX_EXPIRY=3600 make dev

876 tests at 100% coverage; lint, typecheck, build and the SDK package check all pass.

Setting MAX_EXPIRY on a real deployment broke note creation. The API enforces
it per request (as of the earlier hardening work), but the picker still listed
every option, so choosing "30 days" on an instance capped at 7 failed with a
raw 400 — `Maximum expiry is 604800 seconds`, in English, whatever the locale.

Any operator tightening retention hits this. The front simply had no idea what
the server would accept: it builds its config from the environment and never
knew about MAX_EXPIRY at all.

MAX_EXPIRY now flows into ServerConfig, the picker lists only what fits, and
the default lands on the longest allowed option instead of a hardcoded 24h that
a tighter instance would reject. A selection carried over from a looser config
snaps back into range.

Two things found while testing this:

- `MAX_FILE_SIZE=20MB` produced NaN, and `file.size > NaN` is always false —
  size validation silently stopped happening. `MAX_FILE_SIZE=` produced 0,
  rejecting every non-empty file. Parsing now falls back on anything unusable,
  in a pure module (`$lib/server-config.ts`) that the coverage gate measures;
  the route loader was excluded from it, which is why this went unnoticed.

- A blank value aborted API startup: `FOO=` in a .env, or an unset `${FOO}` in
  a compose file, arrives as "" and `Number("")` is 0, which failed the
  positive-integer checks with a message that never mentioned the blank. Blank
  now means "use the default" for every numeric setting, while a value that is
  present but invalid is still rejected.

docker-compose.dev.yml passes MAX_EXPIRY/MAX_FILE_SIZE/MAX_FILES_PER_NOTE
through, so `MAX_EXPIRY=3600 make dev` exercises a tightened instance locally.
Verified that way in a browser: the picker drops to 5 min / 1 hour, defaults to
1 hour, and note creation succeeds instead of 400-ing.
@louisbels
louisbels merged commit b8adcf6 into main Jul 26, 2026
12 checks passed
@louisbels
louisbels deleted the fix/front-respects-server-limits branch July 26, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant