Feat: adjust max file size to have customizable limit - #1648
Conversation
|
@Benebo7 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
I am actually curious about storage limit per workspace specifically on official cloud, but I am not certain about whether it is a architectural decision or a missing config. Whatever, I would be happy to implement it if wanted... |
Yigtwxx
left a comment
There was a problem hiding this comment.
Not a maintainer — just a contributor who has been in these files recently, so take this as input rather than a gate.
The shape of the change looks right to me: the backend stays the enforcement point, the frontend mirrors the value through RuntimeConfig rather than a build-time constant, and the docker/.env.example note that ties MAX_FILE_SIZE_MB to SURFSENSE_MAX_BODY_SIZE is the kind of thing operators actually need — a per-file cap above the proxy's request cap would otherwise fail at Caddy with an opaque error. The ETL_SERVICE placeholder fix is a genuine improvement too; ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING was never a copyable value.
Three things I'd want resolved, plus two follow-ups.
1. Malformed values fail badly on both sides
Left inline on runtime-config.server.tsx:16 and documents_routes.py:62. Short version: ?? and os.getenv(name, default) both only fire on unset, not on empty-string, so MAX_FILE_SIZE_MB= gives 0 on the frontend (nothing can ever be uploaded) and a ValueError at import on the backend (the API doesn't boot). A 500MB typo gives NaN on the frontend, which silently disables the pre-upload check entirely — no toast, no upload, no error.
2. The diff introduces three Biome format errors
Checked against the PR head (5ef02cd) with the repo's own biome.json, on LF copies of the two files:
$ npx @biomejs/biome@2.4.6 check --diagnostic-level=error \
DocumentUploadTab.tsx runtime-config.server.tsx
DocumentUploadTab.tsx format
× Formatter would have printed the following content:
122 122 │ const FOLDER_BATCH_MAX_FILES = 10;
123 123 │
124 │ -
125 │ -
126 124 │ const toggleRowClass =
....... │
147 145 │ const [isFolderUploading, setIsFolderUploading] = useState(false);
148 146 │ const MAX_FILE_SIZE_BYTES = maxFileSizeMB * 1024 * 1024;
149 │ - →
147 │ +
150 148 │ useEffect(() => {
runtime-config.server.tsx format
× Formatter would have printed the following content:
16 │ → → maxFileSizeMB:·Number(process.env.MAX_FILE_SIZE_MB·??·"500"),
│ +
Checked 2 files in 16ms. Found 2 errors.
So: two stray blank lines left behind at DocumentUploadTab.tsx:124-125 where the module-level constants were removed, trailing whitespace on line 149, and a missing trailing comma on runtime-config.server.tsx:16. All three are auto-fixable with npx @biomejs/biome@2.4.6 check --write from surfsense_web/.
Worth fixing before merge rather than after: the biome-check-web hook in .pre-commit-config.yaml sets always_run: true with pass_filenames: false, so it checks the whole surfsense_web tree regardless of what a PR touched. Anything left here doesn't just fail this PR's Quality Gate — it fails the next contributor's too, on a diff that has nothing to do with it.
3. surfsense_backend/.env.example doesn't document the new variable
The PR already edits that file for the ETL_SERVICE fix, but MAX_FILE_SIZE_MB only lands in docker/.env.example. Anyone running the backend outside Docker copies the former and never learns the knob exists. A commented entry next to the other upload-related settings would close that.
Follow-ups
The default 500 now lives in six places — documents_routes.py, runtime-config.server.tsx, and four docker-compose*.yml entries. That's the same frontend/backend drift the PR is trying to remove, just moved up a layer: change one and the two checks disagree again, with the frontend silently accepting a file the backend then rejects at 413. Not necessarily worth restructuring in this PR, but the compose defaults are the ones that actually reach the containers, so they're the pair most worth keeping in lockstep.
Test coverage. surfsense_backend/tests/integration/document_upload/test_upload_limits.py already covers exactly this behaviour, and it hardcodes 500 MB in three places — the module docstring (Max per-file size (500 MB)), test_oversized_file_returns_413 (500 * 1024 * 1024 + 1), and test_file_at_limit_accepted (500 * 1024 * 1024). Once the cap is configurable, that suite fails for anyone who has MAX_FILE_SIZE_MB set in their environment. Deriving the sizes from the configured value would fix that and give the new code its only test at the same time — nothing currently exercises the parsing path.
Happy to help with any of this if it's useful.
| authType: process.env.AUTH_TYPE ?? BUILD_TIME_AUTH_TYPE, | ||
| etlService: process.env.ETL_SERVICE ?? BUILD_TIME_ETL_SERVICE, | ||
| deploymentMode: process.env.DEPLOYMENT_MODE ?? BUILD_TIME_DEPLOYMENT_MODE, | ||
| maxFileSizeMB: Number(process.env.MAX_FILE_SIZE_MB ?? "500") |
There was a problem hiding this comment.
?? only substitutes when the value is null/undefined, so it does not cover the two inputs an operator is most likely to produce:
MAX_FILE_SIZE_MB=(declared but left empty — a half-filled.env) →Number("")is0, soMAX_FILE_SIZE_BYTESbecomes0and every file is treated as oversized. Uploading anything becomes impossible.MAX_FILE_SIZE_MB=500MB(a plausible typo given theMBin the name) →Number("500MB")isNaN. InDocumentUploadTab.addFilesbothf.size > NaNandf.size <= NaNevaluate tofalse, sooversizedis empty (no toast fires) andvalidis empty (the earlyreturnon line 183 hits). The user drops a file and nothing happens at all — no upload, no error, no log.
The NaN case is the nastier of the two because it silently disables the client-side guard rather than failing loudly.
Parsing defensively keeps the default reachable for both:
const parsedMaxFileSizeMB = Number.parseInt(process.env.MAX_FILE_SIZE_MB ?? "", 10);
// ...
maxFileSizeMB: Number.isFinite(parsedMaxFileSizeMB) && parsedMaxFileSizeMB > 0 ? parsedMaxFileSizeMB : 500,This line also needs a trailing comma to satisfy the formatter — details in the review summary.
| # Per-file upload cap. Operators raise MAX_FILE_SIZE_MB when self-hosting on | ||
| # hardware that can take it; the frontend reads the same value for its | ||
| # pre-upload check. | ||
| MAX_FILE_SIZE_BYTES = int(os.getenv("MAX_FILE_SIZE_MB", "500")) * 1024 * 1024 |
There was a problem hiding this comment.
The same two malformed inputs land harder here, because this is module scope.
os.getenv(name, default) returns the default only when the variable is unset. MAX_FILE_SIZE_MB= yields "", and int("") raises ValueError while app.routes.documents_routes is being imported — so a bad value doesn't degrade to the old 500 MB behaviour, it stops the API from booting. MAX_FILE_SIZE_MB=500MB fails identically.
Worth noting the two paths differ in exposure:
- Docker is partly shielded:
${MAX_FILE_SIZE_MB:-500}indocker-compose.ymlsubstitutes for empty and unset. - A local
uv run uvicorn app.app:appreadingsurfsense_backend/.envis not shielded at all. - Neither path is shielded against a typo'd value.
Falling back with a warning keeps the failure recoverable:
def _resolve_max_file_size_mb(default: int = 500) -> int:
raw = os.getenv("MAX_FILE_SIZE_MB", "").strip()
if not raw:
return default
try:
value = int(raw)
except ValueError:
logger.warning("Invalid MAX_FILE_SIZE_MB=%r, falling back to %d MB", raw, default)
return default
if value <= 0:
logger.warning("MAX_FILE_SIZE_MB must be positive, got %d, falling back to %d MB", value, default)
return default
return value
MAX_FILE_SIZE_BYTES = _resolve_max_file_size_mb() * 1024 * 1024logger is already bound a few lines above, so this can stay exactly where the constant is today.
| }); | ||
| }, | ||
| [t] | ||
| [t, MAX_FILE_SIZE_BYTES, maxFileSizeMB] |
There was a problem hiding this comment.
Minor: MAX_FILE_SIZE_BYTES is derived from maxFileSizeMB on every render, so listing both here is redundant — maxFileSizeMB alone already invalidates the callback whenever the byte value can change. [t, maxFileSizeMB] is equivalent and says the intent more directly.
Same thought for the constant itself on line 148: since it's recomputed each render and used in two places (addFiles and useDropzone's maxSize), a useMemo — or just inlining maxFileSizeMB * 1024 * 1024 — would keep the component body free of a shadowed name that used to be a module-level constant. Not blocking either way.
|
@Yigtwxx Thanks for this review, I'm gonna fix that by now |
|
It is probably clean now, and i took this opportunity to fix a biome error due to a wrong positioned import that I made on some PR of mine. |
|
Re-checked The one thing to fix:
|
|
Hope that's enough |
|
I owe you a correction on one thing I said, though.
|
The ruff pre-commit hooks run against every Python file a PR touches, so a lint violation that lands on dev is inherited by the next PR that edits the same file. `app/routes/documents_routes.py` is the live example: its import block is unsorted on dev, and PR #1648 — which only appends a helper function far below the imports — has been failing Backend Quality on `ruff-check` because of it. `ruff check .` reported 21 violations on dev (339fe12): 19 I001, one RUF022, and one UP038. Twenty are `--fix` output. The UP038 is the one hand edit, `isinstance(cookies, (list, tuple))` to `isinstance(cookies, list | tuple)` in the Reddit fetcher, because ruff only offers it as an unsafe fix and the pinned hook runs a plain `--fix`; the two forms are equivalent on the project's Python 3.12 floor. `ruff format` covers eleven further files that had drifted. None of them overlap the files above, so each is pure whitespace. Every import move is alphabetical within its existing group — no import crosses a side-effect boundary, and the deliberately placed asyncio policy block in documents_routes.py is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CpY4T39RzhWyUV9qQpPWw7
Summary
Makes the 500MB per-file upload cap configurable via
MAX_FILE_SIZE_MBinstead of hardcoded, so self-hosted operators can raise it for their
own hardware. Enforced on the backend; the frontend reads the same
value via runtime config so its pre-upload check stays in sync.
Changes
adjust env example to a correct formatation, preventing error from direct copy— fixes an invalidETL_SERVICEplaceholder insurfsense_backend/.env.examplethat broke a direct copy-paste setup.add max file size env providing customizable limitation— introducesMAX_FILE_SIZE_MB, wired through the backend cap and the frontend runtime config.Testing
Verified with
MAX_FILE_SIZE_MBunset (default 500MB) and raised — inboth cases the frontend and backend limits stayed in sync, rejecting
files above the configured cap and accepting files within it end-to-end.
Confirmed the frontend keeps rejecting oversized files even after
raising the limit, until a file at or under the new cap is provided.
Obs
No UI changes so it`s not out of my codebase knowledge...
High-level PR Summary
This PR makes the 500MB per-file upload limit configurable through a new environment variable
MAX_FILE_SIZE_MB, allowing self-hosted operators to adjust the cap based on their hardware capabilities. The change propagates the configurable limit through both backend enforcement and frontend pre-upload checks to keep them synchronized. Additionally, it fixes an invalidETL_SERVICEplaceholder in the backend environment example file that previously caused setup errors when copied directly.⏱️ Estimated Review Time: 5-15 minutes
💡 Review Order Suggestion
docker/.env.examplesurfsense_backend/.env.exampledocker/docker-compose.ymldocker/docker-compose.dev.ymlsurfsense_backend/app/routes/documents_routes.pysurfsense_web/components/providers/runtime-config.server.tsxsurfsense_web/components/providers/runtime-config.tsxsurfsense_web/components/sources/DocumentUploadTab.tsx