From 36d13cf2661b1631ca34abda26956206a9c15c12 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Tue, 21 Jul 2026 10:18:42 -0700 Subject: [PATCH] feat(pages): raise per-file upload limit to 32 MiB for large wasm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An optimized 22 MiB bin_bg.wasm exceeded the 16 MiB per-file cap. Bump max_file 16 -> 32 MiB (still < the 48 MiB total). max_uncompressed_total (48 MiB) and max_compressed / MAX_UPLOAD_BYTES (24 MiB) are left unchanged, so worst-case Worker memory (buffered body + decompressed manifest) stays well under the ~128 MiB isolate ceiling. Note: prefer zstd/gzip over xz for large archives — lzma-rs is slow in wasm and can trip Cloudflare's per-request CPU limit (503 exceededCpu). Bean: pages-yhxp. Co-Authored-By: Claude Opus 4.8 --- ...ile-upload-limit-to-32-mib-for-large-wa.md | 22 +++++++++++++++++++ pages/docs/plans/2026-07-12-pages-design.md | 9 ++++---- pages/src/core/upload.rs | 11 +++++----- 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 pages/.beans/pages-yhxp--raise-per-file-upload-limit-to-32-mib-for-large-wa.md diff --git a/pages/.beans/pages-yhxp--raise-per-file-upload-limit-to-32-mib-for-large-wa.md b/pages/.beans/pages-yhxp--raise-per-file-upload-limit-to-32-mib-for-large-wa.md new file mode 100644 index 0000000..9748cfb --- /dev/null +++ b/pages/.beans/pages-yhxp--raise-per-file-upload-limit-to-32-mib-for-large-wa.md @@ -0,0 +1,22 @@ +--- +# pages-yhxp +title: Raise per-file upload limit to 32 MiB for large wasm binaries +status: completed +type: task +priority: high +created_at: 2026-07-21T17:17:56Z +updated_at: 2026-07-21T17:18:42Z +--- + +User's optimized bin_bg.wasm is 22 MiB, exceeding the 16 MiB per-file cap ("exceeds the maximum per-file size of 16777216 bytes"). A .tar.zst upload decoded fine within the Worker CPU budget (zstd is fast) and correctly hit the per-file limit; the earlier 503 was an xz attempt killed by exceededCpu (lzma-rs too slow in wasm) — zstd is the codec that works. + +Change: bump ONLY max_file 16 -> 32 MiB. Keep max_uncompressed_total (48 MiB) and max_compressed / MAX_UPLOAD_BYTES (24 MiB) unchanged — they already fit a 22 MiB wasm + zst-compressed body, and keeping them bounds worst-case Worker memory (body + decompressed manifest) safely under the ~128 MiB isolate ceiling. 32 < 48 keeps per-file < total. + +Spots: core/upload.rs UploadLimits::default (value + doc comment); design doc §2.2 number. No frontend/api.rs/body-cap change needed (per-file error is server-side only). + +Known latent issue (out of scope, note for follow-up): the xz path can still 503 via exceededCpu on large files because lzma-rs is slow in wasm. Fine as long as users pick zstd/gzip. + +Then: validate (all six), commit, push, deploy. + +## Summary of Changes +Bumped max_file 16 to 32 MiB in UploadLimits::default (fits the user 22 MiB bin_bg.wasm with headroom). Left max_uncompressed_total (48 MiB) and max_compressed / MAX_UPLOAD_BYTES (24 MiB) unchanged so worst-case Worker memory (buffered body + decompressed manifest = 24+48) stays safely under the ~128 MiB isolate ceiling. Updated the default() doc comment and design doc §2.2. zstd is the working codec; xz still risks an exceededCpu 503 on large files (lzma-rs slow in wasm) — noted for follow-up. Validation green: 196 unit + 20 doctests. diff --git a/pages/docs/plans/2026-07-12-pages-design.md b/pages/docs/plans/2026-07-12-pages-design.md index 501440d..f26d4ab 100644 --- a/pages/docs/plans/2026-07-12-pages-design.md +++ b/pages/docs/plans/2026-07-12-pages-design.md @@ -75,11 +75,12 @@ so it should be granted even more sparingly than a single-page grant. `C:\x`), backslash separators, or `..` segments anywhere → reject upload. - **Zip bombs:** enforce **before/while extracting**: max compressed upload 24 MiB (checked via `Content-Length` and while buffering), max total - uncompressed 48 MiB, max 16 MiB per file, max 300 entries, max path length + uncompressed 48 MiB, max 32 MiB per file, max 300 entries, max path length 180 bytes, max depth 10 (raised from 10/25/5 MiB to accommodate large wasm - binaries, still well under the Worker's ~128 MiB memory ceiling). Counting - is done on decompressed bytes as they stream out, aborting on breach — - never trust zip headers alone. + binaries — worst-case memory is compressed + total uncompressed, still well + under the Worker's ~128 MiB isolate ceiling). Counting is done on + decompressed bytes as they stream out, aborting on breach — never trust zip + headers alone. - **Weird entries:** directories are skipped, symlinks don't exist for us (we never touch a filesystem — entries go straight to R2), duplicate entry names → reject, nested zips are stored as opaque files, never recursed. diff --git a/pages/src/core/upload.rs b/pages/src/core/upload.rs index 2410057..d4080ec 100644 --- a/pages/src/core/upload.rs +++ b/pages/src/core/upload.rs @@ -44,17 +44,18 @@ pub struct UploadLimits { impl Default for UploadLimits { /// Production limits (design §2.2, raised to accommodate large wasm /// binaries): 24 MiB max compressed upload, 48 MiB max total uncompressed, - /// 16 MiB max per file, 300 max entries, 180-byte max path length, - /// depth 10. The sizes stay well under the Worker's ~128 MiB memory - /// ceiling — the body is buffered and decompressed in memory — and - /// `max_compressed` must stay in lockstep with + /// 32 MiB max per file, 300 max entries, 180-byte max path length, + /// depth 10. Worst-case memory is bounded by `max_compressed` (the + /// buffered body) plus `max_uncompressed_total` (the decompressed + /// manifest), which stays well under the Worker's ~128 MiB isolate + /// ceiling; `max_compressed` must stay in lockstep with /// [`MAX_UPLOAD_BYTES`](crate::routes::api::MAX_UPLOAD_BYTES). fn default() -> Self { const MIB: u64 = 1024 * 1024; Self { max_compressed: (24 * MIB) as usize, max_uncompressed_total: 48 * MIB, - max_file: 16 * MIB, + max_file: 32 * MIB, max_entries: 300, max_path_len: 180, max_depth: 10,