Skip to content

Feat: adjust max file size to have customizable limit - #1648

Merged
MODSetter merged 4 commits into
MODSetter:devfrom
Benebo7:feat/add-max-file-size
Aug 8, 2026
Merged

Feat: adjust max file size to have customizable limit#1648
MODSetter merged 4 commits into
MODSetter:devfrom
Benebo7:feat/add-max-file-size

Conversation

@Benebo7

@Benebo7 Benebo7 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the 500MB per-file upload cap configurable via MAX_FILE_SIZE_MB
instead 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 invalid ETL_SERVICE placeholder in surfsense_backend/.env.example that broke a direct copy-paste setup.
  • add max file size env providing customizable limitation — introduces MAX_FILE_SIZE_MB, wired through the backend cap and the frontend runtime config.

Testing

Verified with MAX_FILE_SIZE_MB unset (default 500MB) and raised — in
both 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 invalid ETL_SERVICE placeholder in the backend environment example file that previously caused setup errors when copied directly.

⏱️ Estimated Review Time: 5-15 minutes

💡 Review Order Suggestion
Order File Path
1 docker/.env.example
2 surfsense_backend/.env.example
3 docker/docker-compose.yml
4 docker/docker-compose.dev.yml
5 surfsense_backend/app/routes/documents_routes.py
6 surfsense_web/components/providers/runtime-config.server.tsx
7 surfsense_web/components/providers/runtime-config.tsx
8 surfsense_web/components/sources/DocumentUploadTab.tsx

Need help? Join our Discord

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@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.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7606e2c4-6940-437b-b823-1f20182f3098

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Benebo7

Benebo7 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

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 Yigtwxx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 placesdocuments_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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? 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("") is 0, so MAX_FILE_SIZE_BYTES becomes 0 and every file is treated as oversized. Uploading anything becomes impossible.
  • MAX_FILE_SIZE_MB=500MB (a plausible typo given the MB in the name) → Number("500MB") is NaN. In DocumentUploadTab.addFiles both f.size > NaN and f.size <= NaN evaluate to false, so oversized is empty (no toast fires) and valid is empty (the early return on 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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} in docker-compose.yml substitutes for empty and unset.
  • A local uv run uvicorn app.app:app reading surfsense_backend/.env is 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 * 1024

logger is already bound a few lines above, so this can stay exactly where the constant is today.

});
},
[t]
[t, MAX_FILE_SIZE_BYTES, maxFileSizeMB]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Benebo7

Benebo7 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@Yigtwxx Thanks for this review, I'm gonna fix that by now

@Benebo7

Benebo7 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

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.

@Yigtwxx

Yigtwxx commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Re-checked e768895 locally. The fixes look right — the backend now degrades to 500 MB with a warning instead of crashing at import, and the frontend guard survives both MAX_FILE_SIZE_MB= and MAX_FILE_SIZE_MB=500MB. One real CI failure left, and two red checks that are not yours.

The one thing to fix: ruff-format

Backend Quality is failing on documents_routes.py. Reproduced with the pinned hook version (ruff-pre-commit v0.12.5, and 0.15.x agrees):

$ uv run ruff format --diff app/routes/documents_routes.py
--- app/routes/documents_routes.py
+++ app/routes/documents_routes.py
@@ -56,6 +56,7 @@

 router = APIRouter()

+
 # 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.
@@ -66,10 +67,16 @@
     try:
         value = int(raw)
     except ValueError:
-        logger.warning("Invalid MAX_FILE_SIZE_MB=%r, falling back to %d MB", raw, default)
+        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)
+        logger.warning(
+            "MAX_FILE_SIZE_MB must be positive, got %d, falling back to %d MB",
+            value,
+            default,
+        )
         return default
     return value

Two blank lines before the comment block (the comment belongs to the function, so the separation goes above it), and both logger.warning calls exceed the configured line-length = 88. uv run ruff format app/routes/documents_routes.py from surfsense_backend/ applies exactly this.

ruff check is clean on both files you touched — I ran the pinned 0.12.5 against documents_routes.py and test_upload_limits.py and got All checks passed!.

Not caused by this PR

Frontend Quality — the 12 biome errors are all in files this PR does not touch:

components/documents/DocumentsEmptyState.tsx
components/documents/DocumentsSearchResults.tsx
components/documents/DocumentsView.tsx
components/documents/HighlightedText.tsx
lib/documents/documents-view-model.ts
tests/helpers/ui/connector-popup.ts

Extracting those six files straight from current origin/dev and running the pinned npx @biomejs/biome@2.4.6 check --diagnostic-level=error on them reproduces the failures, so dev itself is currently biome-dirty and any PR touching surfsense_web/ inherits this. Your three changed web files pass that same command cleanly.

Integration Tests — one failure, test_converge.py::test_identical_content_at_two_paths_yields_two_documents, with redis.exceptions.ConnectionError: ... connecting to localhost:6379. Infrastructure, not the upload path (230 passed, 1 failed).

Two small notes

surfsense_web/lib/error-toast.ts — that import-order fix already landed on dev in #1666 with the identical reordering. Merging dev into your branch will make it disappear from the diff; no need to carry it here.

useExhaustiveDependencies — biome flags addFiles because MAX_FILE_SIZE_BYTES is now a component-scope useMemo value rather than the module constant it used to be:

components/sources/DocumentUploadTab.tsx:169:19 lint/correctness/useExhaustiveDependencies
  ! This hook does not specify its dependency on MAX_FILE_SIZE_BYTES.

That is my fault — I suggested [t, maxFileSizeMB] while it was still a module constant, and the useMemo changed the rule's answer. It is configured as "warn" and the CI hook runs --diagnostic-level=error, so it will not fail the gate, but [t, maxFileSizeMB, MAX_FILE_SIZE_BYTES] silences it. If you touch that line anyway, maxFileSizeBytes reads better than a SCREAMING_CASE name for something that is no longer a constant.

@Benebo7

Benebo7 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Hope that's enough

@Yigtwxx

Yigtwxx commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

f3e5ecb is right on both counts — ruff-format now passes, and maxFileSizeBytes with [t, maxFileSizeMB, maxFileSizeBytes] clears the biome warning. Nothing left in your diff.

I owe you a correction on one thing I said, though.

ruff check is not clean — and it is not your code

I ran the pinned ruff against documents_routes.py on your branch, which is still based on the older dev, and reported it clean. CI checks out the merge of your branch with current dev, and on that merge the file has a pre-existing I001:

$ git fetch origin refs/pull/1648/merge:m
$ uvx ruff@0.12.5 check --fix --diff <documents_routes.py from that merge>
-from app.knowledge_store.paths import virtual_path_to_doc
 from app.auth.context import AuthContext
 from app.db import (
     ...
 )
+from app.knowledge_store.paths import virtual_path_to_doc

Would fix 1 error.

That misplaced import is on dev already, nowhere near your helper function. Because the hook lints every file a PR touches rather than the lines it changed, your PR inherits it — the same mechanism as the biome failures.

So this was the last red check that could plausibly have been yours, and it is not. Sorry for pointing you at a clean result measured on the wrong tree.

Both dev-side breakages now have PRs

If you would rather not wait on those, moving that single import line in your own branch turns Backend Quality green immediately; it becomes a no-op once #1672 lands. Either way your PR itself is done.

The remaining failure is the Redis ConnectionError in test_converge.py, which is infrastructure.

@MODSetter

Copy link
Copy Markdown
Owner

@Benebo7 @Yigtwxx Thanks

@MODSetter
MODSetter merged commit 48c2f96 into MODSetter:dev Aug 8, 2026
4 of 11 checks passed
MODSetter pushed a commit that referenced this pull request Aug 8, 2026
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
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.

3 participants