Second cause behind #76, and the one that was actually destroying rules.
Node decompresses a gzip response before this code sees the body, but leaves content-length describing the compressed bytes. The blob proxy copied that header onto the longer body it forwarded, so the browser stopped reading exactly that many bytes in and treated the download as complete. Every blob download shared the fault — message source, vCards, signature HTML, forwarded attachments, settings.json — not just Sieve.
The reporter's deployment put Traefik in front of Stalwart, whose compress middleware only engages above 1 KiB. That threshold is why it looked like a race: filter rules one and two were under it, the third pushed the script over, and it came back as 384 bytes of a ~1.3 KB script, cut mid-rule.
The earlier #76 fix does not cover this. It separates "unknown" from "empty" so a save refuses when the script could not be read — but a truncated script is neither. It parses, just with rules missing, so the guard never fires and the next save writes the short version back over the real one.
Reproduced locally: a three-rule script is 1157 bytes plain and 379 gzipped, and the proxied body arrives cut off at exactly the same point the reporter's screenshot shows.
The proxy now asks upstream for identity, so the ordinary case still carries a length the browser can show progress against, and forwards no length at all when a hop compresses anyway. The image proxy is unaffected — it uses node:http, sends no accept-encoding, and never decompresses.
Tests cover the header rule and the end-to-end case of a compressing hop against a script over the threshold.
Second cause behind #76, and the one that was actually destroying rules.
Node decompresses a gzip response before this code sees the body, but leaves `content-length` describing the *compressed* bytes. The blob proxy copied that header onto the longer body it forwarded, so the browser stopped reading exactly that many bytes in and treated the download as complete. Every blob download shared the fault — message source, vCards, signature HTML, forwarded attachments, `settings.json` — not just Sieve.
The reporter's deployment put Traefik in front of Stalwart, whose compress middleware only engages above 1 KiB. That threshold is why it looked like a race: filter rules one and two were under it, the third pushed the script over, and it came back as 384 bytes of a ~1.3 KB script, cut mid-rule.
The earlier #76 fix does not cover this. It separates "unknown" from "empty" so a save refuses when the script could not be read — but a truncated script is neither. It parses, just with rules missing, so the guard never fires and the next save writes the short version back over the real one.
Reproduced locally: a three-rule script is 1157 bytes plain and 379 gzipped, and the proxied body arrives cut off at exactly the same point the reporter's screenshot shows.
The proxy now asks upstream for `identity`, so the ordinary case still carries a length the browser can show progress against, and forwards no length at all when a hop compresses anyway. The image proxy is unaffected — it uses `node:http`, sends no `accept-encoding`, and never decompresses.
Tests cover the header rule and the end-to-end case of a compressing hop against a script over the threshold.
**Merged** 2026-08-30 as coffey-labs/ihasmail@ef7823d6de65
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Second cause behind #76, and the one that was actually destroying rules.
Node decompresses a gzip response before this code sees the body, but leaves
content-lengthdescribing the compressed bytes. The blob proxy copied that header onto the longer body it forwarded, so the browser stopped reading exactly that many bytes in and treated the download as complete. Every blob download shared the fault — message source, vCards, signature HTML, forwarded attachments,settings.json— not just Sieve.The reporter's deployment put Traefik in front of Stalwart, whose compress middleware only engages above 1 KiB. That threshold is why it looked like a race: filter rules one and two were under it, the third pushed the script over, and it came back as 384 bytes of a ~1.3 KB script, cut mid-rule.
The earlier #76 fix does not cover this. It separates "unknown" from "empty" so a save refuses when the script could not be read — but a truncated script is neither. It parses, just with rules missing, so the guard never fires and the next save writes the short version back over the real one.
Reproduced locally: a three-rule script is 1157 bytes plain and 379 gzipped, and the proxied body arrives cut off at exactly the same point the reporter's screenshot shows.
The proxy now asks upstream for
identity, so the ordinary case still carries a length the browser can show progress against, and forwards no length at all when a hop compresses anyway. The image proxy is unaffected — it usesnode:http, sends noaccept-encoding, and never decompresses.Tests cover the header rule and the end-to-end case of a compressing hop against a script over the threshold.
Merged 2026-08-30 as coffey-labs/ihasmail@ef7823d6de
Rebuilt from: git history, session transcript.