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.tsx → handleTextareaPaste |
Extracts pasted image files and calls uploadFiles(imageFiles) directly. |
frontend/src/hooks/files/useChatFileSources.tsx → handleSelectedFiles / 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
Required tests
Add regression coverage for at least:
- 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
useFileDropzone:
- direct
uploadFiles([oversizedFile]) does not call useCreateChat or fetchUploadFile
- error uses
maxSizeFormatted
- valid files still upload
- Selection entry points:
FileUploadButton
useConversationDropzone
- pasted image handling
useChatFileSources.onSelectFiles
- 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
- Outlook direct upload:
- an invalid selected email/attachment does not call
fetchUploadFile
- HTTP 413 is surfaced as the standard actionable error
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.
Goal
Prevent every local
Filelarger 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
maxSizeBytesandmaxSizeFormattedfromuseUploadFeature()/FeatureConfigProvider. Preserve any stricter supported-file-type limit when one applies.Scope
This issue covers local multipart uploads to
POST /api/v1beta/me/filesin:FileobjectsOut of scope:
fetchLinkFile(the browser does not possess the remote bytes before linking)Why this remains open
Client-side protection exists, but only on some
react-dropzoneinstances. The API-facing upload functions do not enforce the limit, so any caller that suppliesFile[]directly bypasses validation and transmits the oversized file.react-dropzonedefaults an omittedmaxSizetoInfinity.Existing protection that must be preserved
frontend/src/hooks/files/useFileDropzone.tsuseDropzonepassesmaxSize: getMaxFileSize().file-too-largeintoUploadTooLargeError(maxSizeFormatted).isUploadTooLarge()for numeric/string 413 responses.frontend/src/hooks/files/useChatFileSources.tsxmaxSize: maxSizeBytesand handlesfile-too-large.frontend/src/hooks/files/errors.tsUploadTooLargeErroralready supports a formatted runtime limit.isUploadTooLarge()already recognizes HTTP 413, including the Firefox fallback.backend/erato/src/server/api/v1beta/mod.rsupload_filestreams each multipart field with the configured per-file limit and returns 413 when a file exceeds it.Verified bypasses
frontend/src/components/ui/FileUpload/FileUploadButton.tsxuseDropzonewith nomaxSizeand no size-rejection handling, then passes accepted files directly toperformFileUpload. Used by the normal desktop chat button and other surfaces.frontend/src/hooks/files/useFileDropzone.ts→ returneduploadFiles(files)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.tsmaxSize; it calls the supplieduploadFilesdirectly. Used by the main web conversation and Office add-in conversation hosts.frontend/src/components/ui/Chat/ChatInput.tsx→handleTextareaPasteuploadFiles(imageFiles)directly.frontend/src/hooks/files/useChatFileSources.tsx→handleSelectedFiles/ returnedonSelectFilesfrontend/src/components/ui/FileUpload/FileUpload.tsxwithbuttonOnlyFileUploadButton, so the validated dropzone returned byuseFileDropzoneis not the input that selects the file.frontend/src/hooks/files/useStandaloneFileUpload.tsFormDataand callsfetchUploadFilewithout preflight size validation. Its 413 path constructsUploadTooLargeError()without the configured limit, producingMaximum size: —.frontend/src/components/ui/FileUpload/AssistantFileUploadSelector.tsxmaxSize; both delegate touseStandaloneFileUpload.office-addin/src/outlook/components/AddinChatInput.tsxfetchUploadFiledirectly 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:
size > maxSizeBytesis invalid.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— beforesetUploading, silent-chat creation,FormData, orfetchUploadFileuseStandaloneFileUpload.uploadFiles— before upload state,FormData, orfetchUploadFileAddinChatInput— before constructing/sending the multipart requestThis must automatically protect button selection, conversation drag/drop, pasted images, generated Teams/Outlook files, custom
onSelectFilesimplementations, and direct programmatic calls.3. Keep immediate validation on selection surfaces
Pass the effective configured limit to every local-file
useDropzonewhere possible, including:FileUploadButtonuseConversationDropzoneAssistantFileUploadSelectorHandle
file-too-largerejections 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
maxSizeFormattedvalue everywhere; never display a hard-coded20 MB.File is too large. Maximum size: {maxSize}.Improve it with guidance such as selecting or creating a smaller file.Maximum size: —outcome from standalone/assistant uploads.5. Show the configured limit proactively
Maximum file size: {maxSizeFormatted}to full dropzone/helper UI.Acceptance criteria
fetchUploadFilewhen any selected file is larger than the runtime-configured limit.Fileobjects all enforce the same configured limit.fetchUploadFile.Required tests
Add regression coverage for at least:
useFileDropzone:uploadFiles([oversizedFile])does not calluseCreateChatorfetchUploadFilemaxSizeFormattedFileUploadButtonuseConversationDropzoneuseChatFileSources.onSelectFilesfetchUploadFile—fetchUploadFileisUploadTooLarge: numeric 413, string"413", unrelated errors, and the existing Firefox fallback.Update existing mocks of
useUploadFeature()to provideenabled,maxSizeBytes, andmaxSizeFormatted; 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/srcrg -n 'fetchUploadFile|uploadFiles\s*\(' frontend/src office-addin/srcrg -n 'maxSize:|file\.size|UploadTooLarge|isUploadTooLarge' frontend/src office-addin/srcRun at minimum:
cd frontend && pnpm test --run src/hooks/files src/components/ui/FileUpload src/components/ui/Chat/ChatInput.test.tsxcd frontend && pnpm typecheckcd office-addin && pnpm testcd office-addin && pnpm typecheckDuring the 2026-08-10 audit, the three existing focused frontend suites (
useFileDropzone,useFileUploadWithTokenCheck, andFileUploadButton) 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.