Skip to content

feat: enforce configured file-size limits across all local upload paths - #934

Draft
bdart wants to merge 7 commits into
mainfrom
feat/enforce-file-size-limits-bdf9
Draft

feat: enforce configured file-size limits across all local upload paths#934
bdart wants to merge 7 commits into
mainfrom
feat/enforce-file-size-limits-bdf9

Conversation

@bdart

@bdart bdart commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes ERMAIN-34.

Prevents every local File larger than the runtime-configured per-file upload limit from starting an upload, regardless of how the file entered the UI. The configured limit (maxSizeBytes / maxSizeFormatted from useUploadFeature()) is enforced consistently — no hard-coded 20 MB anywhere in client code.

What changed

New utility — frontend/src/utils/validateFileSizes.ts

Pure, side-effect-free preflight function. Accepts File[] and maxSizeBytes; returns a discriminated union with { valid: true } or { valid: false; oversizedFiles: File[] }. One byte above the limit → invalid; exactly at the limit → valid.

API-facing upload hooks (correctness boundary)

File Change
useFileDropzone.uploadFiles Size preflight before setUploading, silent-chat creation, FormData, or fetchUploadFile.
useStandaloneFileUpload.uploadFiles Imports useUploadFeature; size preflight before upload state or network request; UploadTooLargeError now always receives maxSizeFormatted (fixes the Maximum size: — regression on 413 responses).
useChatFileSources.handleSelectedFiles (onSelectFiles) Size preflight before performDiskUpload protects the host/custom File[] path used by Outlook/Teams add-menu.

Selection surfaces (UX early-rejection)

File Change
FileUploadButton Reads maxSizeBytes from useUploadFeature; passes to useDropzone; adds onError prop; routes file-too-large rejections via onError.
FileUpload (buttonOnly) Extracts setError from useFileDropzone and passes it as onError to FileUploadButton.
useConversationDropzone Adds optional maxSize, maxSizeFormatted, and onError props; surfaces file-too-large rejections immediately via onError.
AssistantFileUploadSelector Reads limit from useUploadFeature; passes maxSize to its hidden disk-picker dropzone; routes file-too-large into its cloudLinkError state.
Chat.tsx Passes maxSize, maxSizeFormatted, and setUploadError as onError to useConversationDropzone.
OutlookAddinChat + AddinChatCore Same useConversationDropzone wiring for both add-in conversation hosts.

Outlook direct email-source upload (AddinChatInput)

  • After resolveSelectedFilesForSend(), validates file sizes with validateFileSizes before constructing FormData or calling fetchUploadFile. Oversized files set emailUploadError and skip the upload (message is still sent without the file IDs; chips remain for retry).
  • In the catch block, isUploadTooLarge(error) maps HTTP 413 to UploadTooLargeError(maxSizeFormatted) instead of silently logging.
  • emailUploadError is merged into chatInputProps.uploadError and surfaced through AddinChatInputCoreChatInput.

Library exports

UploadTooLargeError, isUploadTooLarge, and validateFileSizes are now exported from @erato/frontend/library so add-in packages can consume them directly.

Test environment fix

Added frontend/src/__mocks__/desktop-sidecar-protocol.ts stub + vitest alias so tests that import through the library chain don't fail on the missing native package.

Tests added / updated

File Coverage
utils/__tests__/validateFileSizes.test.ts 8 tests: boundary conditions (−1 byte, exact, +1 byte), batch rejection with one oversized file, multi-oversized batch, metadata (original File reference returned)
hooks/files/__tests__/useFileDropzone.test.tsx 6 new tests: size preflight blocks fetchUploadFile and chat creation, sets UploadTooLargeError with formatted limit, accepts exact-limit file, rejects mixed batch, isUploading stays false
hooks/files/__tests__/useStandaloneFileUpload.test.tsx 7 new tests: preflight rejection, formatted limit (no ), exact-limit accept, mixed-batch rejection, isUploading stays false, HTTP 413 with numeric and string status
hooks/files/__tests__/useConversationDropzone.test.tsx 6 new tests: backward-compatible no-maxSize mode, maxSize forwarded to useDropzone, file-too-largeUploadTooLargeError via onError, no uploadFiles call on rejection, valid files still upload
hooks/files/__tests__/useChatFileSources.test.tsx 4 new tests: onSelectFiles preflight rejects oversized, formats limit correctly, accepts exact limit, rejects mixed batch
components/ui/FileUpload/__tests__/FileUploadButton.test.tsx 4 new tests: maxSize from feature config passed to dropzone, file-too-largeUploadTooLargeError via onError, no performFileUpload on rejection, no onError on clean drop

Scope not covered in this PR (per issue)

  • Cloud linking via fetchLinkFile (browser does not possess remote bytes)
  • Audio transcription/dictation WebSocket protocols
  • Backend limit changes
  • File compression
  • Automatic retry of oversized files

Testing

All affected test suites pass (vitest run). The FileUpload component tests, Chat tests, provider tests, and utility tests are unaffected.

Linear Issue: ERMAIN-34

Open in Web Open in Cursor 

cursoragent and others added 7 commits August 10, 2026 17:23
Co-authored-by: Daniel <bdart@users.noreply.github.com>
- useFileDropzone.uploadFiles: validate sizes before setUploading, silent-chat
  creation, or any network request; add maxSizeBytes/maxSizeFormatted to deps
- useStandaloneFileUpload: import useUploadFeature, add size preflight before
  FormData construction, pass maxSizeFormatted to UploadTooLargeError (fixes
  the 'Maximum size: -' regression on 413 responses)
- useConversationDropzone: add optional maxSize, maxSizeFormatted, and onError
  params; surface file-too-large rejections immediately via onError
- useChatFileSources.handleSelectedFiles (onSelectFiles): validate sizes before
  calling performDiskUpload to protect the host/custom File-object path

Co-authored-by: Daniel <bdart@users.noreply.github.com>
… error state

- FileUploadButton: read maxSizeBytes from useUploadFeature, pass to useDropzone;
  add onError prop; surface file-too-large dropzone rejections via onError
- FileUpload (buttonOnly): extract setError from useFileDropzone and pass as
  onError to FileUploadButton so rejections update the visible error state
- AssistantFileUploadSelector: read maxSizeBytes/maxSizeFormatted from
  useUploadFeature, pass maxSize to its hidden disk-picker dropzone, handle
  file-too-large rejection into the local cloudLinkError state
- Chat.tsx: read maxSizeBytes/maxSizeFormatted from useUploadFeature; import
  useFileUploadStore to get setError; pass maxSize, maxSizeFormatted, and
  onError to useConversationDropzone so drag-drop rejections are visible

Co-authored-by: Daniel <bdart@users.noreply.github.com>
… from library

Makes these available to office-addin consumers via @erato/frontend/library

Co-authored-by: Daniel <bdart@users.noreply.github.com>
…aths

AddinChatInput.wrappedOnSendMessage:
- After resolveSelectedFilesForSend(), validate file sizes with validateFileSizes
  before constructing FormData or calling fetchUploadFile; if any file exceeds
  the configured limit, set emailUploadError and fall through (message sent
  without file IDs, chips remain for retry)
- In the catch block, map isUploadTooLarge(error) to UploadTooLargeError with
  the runtime-formatted limit instead of silently logging; surface via
  emailUploadError state merged onto chatInputProps.uploadError

OutlookAddinChat + AddinChatCore:
- Import useUploadFeature and useFileUploadStore
- Pass maxSize, maxSizeFormatted, and onError to useConversationDropzone so
  drag-drop size rejections are immediately visible in the add-in UI

Co-authored-by: Daniel <bdart@users.noreply.github.com>
New test files:
- utils/__tests__/validateFileSizes.test.ts: 8 tests covering boundary
  conditions (1-byte below/at/above limit), batch rejection, and metadata
- hooks/files/__tests__/useStandaloneFileUpload.test.tsx: preflight
  rejection, formatted limit in errors (not '--'), HTTP 413 fallback
- hooks/files/__tests__/useConversationDropzone.test.tsx: maxSize
  passed to useDropzone, file-too-large routing via onError
- hooks/files/__tests__/useChatFileSources.test.tsx: onSelectFiles
  preflight for host/custom file paths

Updated test files:
- hooks/files/__tests__/useFileDropzone.test.tsx: update FeatureConfigProvider
  mock to include maxSizeBytes/maxSizeFormatted; add 6 preflight tests
- components/ui/FileUpload/__tests__/FileUploadButton.test.tsx: add 4
  tests for maxSize configuration and file-too-large rejection routing

Co-authored-by: Daniel <bdart@users.noreply.github.com>
The new size-limit tests allocated real multi-MiB buffers, so vitest's
deep-equality in toHaveBeenCalledWith walked every byte (~3.9s per
assertion) and two tests exceeded the 5s timeout. Share a fixture that
reports a size without allocating it.

Also fixes the tsc errors that broke both Docker builds (invalid
FileCapability fixtures, untyped vi.fn mocks), the eslint/prettier
violations, and the office-addin library mocks that lacked the
useUploadFeature/useFileUploadStore exports the add-in hosts now read.

Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
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.

2 participants