Skip to content

fix(uploads): bound multipart body read in workspace-file and knowledge-document upload routes#5413

Merged
waleedlatif1 merged 3 commits into
stagingfrom
fix-bound-upload-formdata
Jul 4, 2026
Merged

fix(uploads): bound multipart body read in workspace-file and knowledge-document upload routes#5413
waleedlatif1 merged 3 commits into
stagingfrom
fix-bound-upload-formdata

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • workspace files upload route and v1 knowledge document upload route now read the multipart body through the existing readFormDataWithLimit helper instead of raw request.formData(), matching the pattern already used by files/upload and v1/files
  • existing post-parse size checks are kept as-is (defense in depth)

Type of Change

  • Bug fix

Testing

Added route tests covering: declared content-length over the limit rejected immediately, chunked/no-content-length body over the limit rejected once the streaming cap trips, and a normal under-limit upload still succeeding. Ran the full route test suites, typecheck, biome, and bun run check:api-validation — all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 4, 2026 7:16pm

Request Review

@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Hardens upload parsing against oversized bodies without changing auth or storage logic; existing post-parse size checks remain as defense in depth.

Overview
Workspace file upload (POST /api/workspaces/[id]/files) and v1 knowledge document upload (POST /api/v1/knowledge/[id]/documents) now parse multipart bodies with readFormDataWithLimit instead of unbounded request.formData(), aligned with files/upload and v1/files.

Caps use the existing file size limits plus a shared MAX_MULTIPART_OVERHEAD_BYTES (1MB) exported from stream-limits; duplicate local constants were removed from those other upload routes. Oversized bodies return 413 with the limit error message; invalid multipart still returns 400. Per-file size checks after parse are unchanged.

New route tests cover declared Content-Length over the cap, chunked bodies without Content-Length that trip the stream cap, and successful under-limit uploads.

Reviewed by Cursor Bugbot for commit f8f83b8. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two upload routes — workspaces/[id]/files and v1/knowledge/[id]/documents — that were reading multipart bodies via the unbounded request.formData(), making them vulnerable to oversized payloads. Both now go through the existing readFormDataWithLimit helper, matching the pattern already in files/upload and v1/files. As a prerequisite, MAX_MULTIPART_OVERHEAD_BYTES is lifted out of each call site and exported from stream-limits.ts as a single source of truth.

  • Replaced request.formData() with readFormDataWithLimit in both new routes, with 413 responses on overflow and 400 on malformed multipart.
  • Centralized MAX_MULTIPART_OVERHEAD_BYTES = 1 MB into stream-limits.ts and updated all four upload routes to import it from there.
  • Added route-level test suites for both updated routes, covering declared-content-length rejection, chunked-body overflow, and successful under-limit uploads.

Confidence Score: 5/5

Safe to merge — the change is a targeted, well-tested hardening of two upload routes with no behavioral changes outside the new overflow guard.

Both routes now share the same bounded-read helper already proven in the two sibling upload routes. The centralized constant eliminates drift, the error handling is consistent (413 on overflow, 400 on malformed multipart), and the existing post-parse file-size checks are kept intact. New test suites cover all three important cases for each route with no gaps in the logic.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/lib/core/utils/stream-limits.ts Exports the new MAX_MULTIPART_OVERHEAD_BYTES constant (1 MB) so all upload routes share a single definition; no logic changes to existing helpers.
apps/sim/app/api/workspaces/[id]/files/route.ts Replaces unbounded request.formData() with readFormDataWithLimit; adds 413/400 error handling and imports MAX_MULTIPART_OVERHEAD_BYTES from the shared module. Existing post-parse size check is retained for defense-in-depth.
apps/sim/app/api/v1/knowledge/[id]/documents/route.ts Same bounded-read fix as the workspace route; MAX_FILE_SIZE + MAX_MULTIPART_OVERHEAD_BYTES caps the 100 MB knowledge-document limit correctly, and the secondary file.size guard is preserved.
apps/sim/app/api/files/upload/route.ts Minor cleanup only: removes the now-duplicated local MAX_MULTIPART_OVERHEAD_BYTES declaration and imports it from stream-limits.
apps/sim/app/api/v1/files/route.ts Same import-consolidation cleanup as files/upload; no behavioral change.
apps/sim/app/api/v1/knowledge/[id]/documents/route.test.ts New test suite with three cases: declared content-length rejection, chunked-body overflow via pull-based stream, and successful under-limit upload.
apps/sim/app/api/workspaces/[id]/files/route.test.ts New test suite mirroring the knowledge-document tests; mocks MAX_WORKSPACE_FORMDATA_FILE_SIZE to 1 KB to keep threshold low and tests realistic, all three cases covered.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Route as Upload Route (POST)
    participant Helper as readFormDataWithLimit
    participant Native as request.formData()
    participant Stream as readStreamToBufferWithLimit

    Client->>Route: POST (multipart body)
    Route->>Route: Auth / permissions check
    Route->>Helper: "readFormDataWithLimit(request, { maxBytes })"
    Helper->>Helper: assertContentLengthWithinLimit()
    alt content-length present
        alt "content-length > maxBytes"
            Helper-->>Route: throw PayloadSizeLimitError
            Route-->>Client: 413 payload too large
        else content-length within limit
            Helper->>Native: request.formData()
            Native-->>Helper: FormData
        end
    else no content-length + body present
        Helper->>Stream: readStreamToBufferWithLimit(body, maxBytes)
        alt "streamed bytes > maxBytes"
            Stream-->>Helper: throw PayloadSizeLimitError
            Helper-->>Route: throw
            Route-->>Client: 413 payload too large
        else within limit
            Stream-->>Helper: Buffer
            Helper->>Native: new Request(...).formData()
            Native-->>Helper: FormData
        end
    end
    Helper-->>Route: FormData
    Route->>Route: Post-parse file.size check (defense-in-depth)
    Route->>Route: Upload / index file
    Route-->>Client: 200 success
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Route as Upload Route (POST)
    participant Helper as readFormDataWithLimit
    participant Native as request.formData()
    participant Stream as readStreamToBufferWithLimit

    Client->>Route: POST (multipart body)
    Route->>Route: Auth / permissions check
    Route->>Helper: "readFormDataWithLimit(request, { maxBytes })"
    Helper->>Helper: assertContentLengthWithinLimit()
    alt content-length present
        alt "content-length > maxBytes"
            Helper-->>Route: throw PayloadSizeLimitError
            Route-->>Client: 413 payload too large
        else content-length within limit
            Helper->>Native: request.formData()
            Native-->>Helper: FormData
        end
    else no content-length + body present
        Helper->>Stream: readStreamToBufferWithLimit(body, maxBytes)
        alt "streamed bytes > maxBytes"
            Stream-->>Helper: throw PayloadSizeLimitError
            Helper-->>Route: throw
            Route-->>Client: 413 payload too large
        else within limit
            Stream-->>Helper: Buffer
            Helper->>Native: new Request(...).formData()
            Native-->>Helper: FormData
        end
    end
    Helper-->>Route: FormData
    Route->>Route: Post-parse file.size check (defense-in-depth)
    Route->>Route: Upload / index file
    Route-->>Client: 200 success
Loading

Reviews (4): Last reviewed commit: "fix(uploads): share MAX_MULTIPART_OVERHE..." | Re-trigger Greptile

Comment thread apps/sim/app/api/v1/knowledge/[id]/documents/route.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Fixed — MAX_MULTIPART_OVERHEAD_BYTES is now exported once from @/lib/core/utils/stream-limits and all four upload routes (files/upload, v1/files, workspaces/[id]/files, v1/knowledge/[id]/documents) import it instead of redeclaring locally.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

1 similar comment
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f8f83b8. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f8f83b8. Configure here.

@waleedlatif1 waleedlatif1 merged commit 1a371e5 into staging Jul 4, 2026
18 checks passed
@waleedlatif1 waleedlatif1 deleted the fix-bound-upload-formdata branch July 4, 2026 22:32
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