Skip to content

Enforce configured file-size limits across all local upload paths #140

Description

@bdart

Goal

Prevent every local File larger than the runtime-configured per-file upload limit from starting an upload, regardless of how the file entered the UI. Give the user the configured limit before upload where practical and a consistent, actionable error for both client-side rejection and backend HTTP 413 fallback responses.

The limit is configurable. Do not hard-code 20 MB. Read maxSizeBytes and maxSizeFormatted from useUploadFeature() / FeatureConfigProvider. Preserve any stricter supported-file-type limit when one applies.

Scope

This issue covers local multipart uploads to POST /api/v1beta/me/files in:

  • The main web chat
  • Assistant default-file uploads
  • Shared/exported frontend upload components and hooks
  • Outlook and Teams add-in flows that ultimately upload local or generated File objects

Out of scope:

  • Cloud linking through fetchLinkFile (the browser does not possess the remote bytes before linking)
  • Audio transcription/dictation WebSocket protocols
  • Changing the backend limit
  • File compression
  • Automatically retrying an unchanged oversized file; this failure is deterministic, so the user should be asked to select or create a smaller file

Why this remains open

Client-side protection exists, but only on some react-dropzone instances. The API-facing upload functions do not enforce the limit, so any caller that supplies File[] directly bypasses validation and transmits the oversized file.

react-dropzone defaults an omitted maxSize to Infinity.

Existing protection that must be preserved

  • frontend/src/hooks/files/useFileDropzone.ts
    • Its own useDropzone passes maxSize: getMaxFileSize().
    • Its drop callback translates file-too-large into UploadTooLargeError(maxSizeFormatted).
    • Its HTTP error path uses isUploadTooLarge() for numeric/string 413 responses.
  • frontend/src/hooks/files/useChatFileSources.tsx
    • Its default disk-picker dropzone passes maxSize: maxSizeBytes and handles file-too-large.
  • frontend/src/hooks/files/errors.ts
    • UploadTooLargeError already supports a formatted runtime limit.
    • isUploadTooLarge() already recognizes HTTP 413, including the Firefox fallback.
  • backend/erato/src/server/api/v1beta/mod.rs
    • upload_file streams each multipart field with the configured per-file limit and returns 413 when a file exceeds it.
    • The enclosing request body limit is deliberately larger; client validation should therefore remain per-file, not sum the selected files.

Verified bypasses

Entry path Current behavior / root cause
frontend/src/components/ui/FileUpload/FileUploadButton.tsx Creates a separate useDropzone with no maxSize and no size-rejection handling, then passes accepted files directly to performFileUpload. Used by the normal desktop chat button and other surfaces.
frontend/src/hooks/files/useFileDropzone.ts → returned uploadFiles(files) Checks file capabilities/types, but never checks file.size. Validation on the hook's own dropzone does not protect direct callers. It can also create a silent chat before an oversized file is rejected by the server.
frontend/src/hooks/files/useConversationDropzone.ts Its dropzone has no maxSize; it calls the supplied uploadFiles directly. Used by the main web conversation and Office add-in conversation hosts.
frontend/src/components/ui/Chat/ChatInput.tsxhandleTextareaPaste Extracts pasted image files and calls uploadFiles(imageFiles) directly.
frontend/src/hooks/files/useChatFileSources.tsxhandleSelectedFiles / returned onSelectFiles Host/custom components can supply already-resolved files directly. Outlook/Teams add-menu content uses this route.
frontend/src/components/ui/FileUpload/FileUpload.tsx with buttonOnly Renders FileUploadButton, so the validated dropzone returned by useFileDropzone is not the input that selects the file.
frontend/src/hooks/files/useStandaloneFileUpload.ts Builds FormData and calls fetchUploadFile without preflight size validation. Its 413 path constructs UploadTooLargeError() without the configured limit, producing Maximum size: —.
frontend/src/components/ui/FileUpload/AssistantFileUploadSelector.tsx Both its hidden disk-picker dropzone and its plain-button branch omit maxSize; both delegate to useStandaloneFileUpload.
office-addin/src/outlook/components/AddinChatInput.tsx Calls fetchUploadFile directly for resolved email-source files. validateAttachment() adds preview warnings, but invalid rows may remain selected and the upload is not blocked. Its direct catch does not map 413 to the standard user-facing error.

Required implementation

1. Add an authoritative shared preflight validator

Create or reuse a pure helper that accepts File[] plus the configured byte/formatted limit and reports oversized files. It must be usable from the core frontend and exported for Office add-in consumers if needed.

Rules:

  • Check each file independently against the configured per-file limit.
  • A file exactly equal to the limit is valid; size > maxSizeBytes is invalid.
  • If any file in a batch is oversized, reject the entire batch and start no network or chat-creation request.
  • Return enough information to produce a localized, actionable error containing the configured limit and, where useful, the offending filename(s).
  • Preserve stricter per-type limits where existing code applies them.

2. Enforce preflight at API-facing boundaries

Dropzone validation is an early UX optimization, not the security/correctness boundary. At minimum, call the shared preflight before side effects in:

  • useFileDropzone.uploadFiles — before setUploading, silent-chat creation, FormData, or fetchUploadFile
  • useStandaloneFileUpload.uploadFiles — before upload state, FormData, or fetchUploadFile
  • The direct Outlook email-source upload in AddinChatInput — before constructing/sending the multipart request

This must automatically protect button selection, conversation drag/drop, pasted images, generated Teams/Outlook files, custom onSelectFiles implementations, and direct programmatic calls.

3. Keep immediate validation on selection surfaces

Pass the effective configured limit to every local-file useDropzone where possible, including:

  • FileUploadButton
  • useConversationDropzone
  • AssistantFileUploadSelector

Handle file-too-large rejections explicitly and route them into the owning error state. Do not rely only on the dropzone layer; direct callers still require the shared preflight.

4. Normalize the user experience

  • Use the runtime maxSizeFormatted value everywhere; never display a hard-coded 20 MB.
  • Use localized copy. The existing baseline is File is too large. Maximum size: {maxSize}. Improve it with guidance such as selecting or creating a smaller file.
  • Remove the Maximum size: — outcome from standalone/assistant uploads.
  • Surface the same class of message when the backend still returns 413 (configuration races, proxies, or future unvalidated callers).
  • Do not automatically retry an oversized upload.
  • Keep the selected/staged file available when appropriate so the user can remove or replace it.

5. Show the configured limit proactively

  • Add Maximum file size: {maxSizeFormatted} to full dropzone/helper UI.
  • For compact/icon-only buttons and source selectors, expose the limit in the visible label, menu description, or tooltip and in an accessible label/description.
  • Existing Outlook staged-file previews may retain their per-file warnings, but an invalid selected file must also block the request.

Acceptance criteria

  • No local multipart upload path starts fetchUploadFile when any selected file is larger than the runtime-configured limit.
  • Rejecting an oversized file happens before silent-chat creation or any other upload-related network request.
  • A file exactly equal to the configured limit is accepted.
  • A mixed batch containing an oversized file is rejected atomically; no files from that batch are transmitted.
  • The normal desktop chat button, mobile/unified disk picker, conversation drag/drop, pasted images, and host-provided/generated File objects all enforce the same configured limit.
  • Assistant disk uploads enforce the same limit with and without cloud providers/custom selectors.
  • Outlook direct email-source uploads block invalid selected files before fetchUploadFile.
  • Client-side rejection shows a localized, actionable message containing the runtime-formatted limit.
  • Numeric or string HTTP 413 responses still produce the same user-facing too-large error across chat, assistant, and direct Outlook upload implementations.
  • Upload UI communicates the runtime-configured maximum before upload; no user-facing code assumes 20 MB.
  • Cloud-linking and audio WebSocket behavior is unchanged.

Required tests

Add regression coverage for at least:

  1. Shared preflight/helper:
    • one byte below, exactly at, and one byte above a non-default limit (for example 15 MiB)
    • multiple files with one oversized file
    • offending filenames/error data
  2. useFileDropzone:
    • direct uploadFiles([oversizedFile]) does not call useCreateChat or fetchUploadFile
    • error uses maxSizeFormatted
    • valid files still upload
  3. Selection entry points:
    • FileUploadButton
    • useConversationDropzone
    • pasted image handling
    • useChatFileSources.onSelectFiles
  4. Standalone/assistant uploads:
    • oversized file does not call fetchUploadFile
    • 413 error includes the configured limit rather than
    • both source-selector and plain-button branches are covered
  5. Outlook direct upload:
    • an invalid selected email/attachment does not call fetchUploadFile
    • HTTP 413 is surfaced as the standard actionable error
  6. isUploadTooLarge: numeric 413, string "413", unrelated errors, and the existing Firefox fallback.

Update existing mocks of useUploadFeature() to provide enabled, maxSizeBytes, and maxSizeFormatted; several current tests return only { enabled: true } and therefore cannot verify this behavior.

Verification

Use repository searches to confirm every upload boundary and dropzone has been considered:

  • rg -n 'useDropzone\s*\(' frontend/src office-addin/src
  • rg -n 'fetchUploadFile|uploadFiles\s*\(' frontend/src office-addin/src
  • rg -n 'maxSize:|file\.size|UploadTooLarge|isUploadTooLarge' frontend/src office-addin/src

Run at minimum:

  • cd frontend && pnpm test --run src/hooks/files src/components/ui/FileUpload src/components/ui/Chat/ChatInput.test.tsx
  • cd frontend && pnpm typecheck
  • cd office-addin && pnpm test
  • cd office-addin && pnpm typecheck

During the 2026-08-10 audit, the three existing focused frontend suites (useFileDropzone, useFileUploadWithTokenCheck, and FileUploadButton) passed 18/18 tests, but no existing test covered file-size rejection or 413 behavior. New tests are required; a green pre-change suite does not demonstrate completion.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions