Skip to content

fix(attachments): enforce the 3-file limit on every attach path - #1048

Merged
miurla merged 4 commits into
mainfrom
fix/enforce-attachment-limit
Sep 30, 2026
Merged

miurla merged 4 commits into
mainfrom
fix/enforce-attachment-limit

Conversation

@miurla

@miurla miurla commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

The composer is supposed to allow at most 3 attachments per message, but the limit was not actually enforced. Only one of the three attach paths checked it, and the server did not check it at all, so a single message could carry far more than 3 files.

Attach path Before After
Drag and drop Checked existing + new against 3 Unchanged (now uses the shared constant)
File picker (📎) Took the first 3 of each selection, then appended them to files already in the composer. Picking files twice gave 6 Only fills the remaining slots and shows an error toast for the rest
Library picker No count check at all Refuses the attach once 3 files are in the composer
/api/chat No check Returns 400 bad_request when a submitted message has more than 3 file parts. For guests every message in messages is checked, since that is the history the model receives. A regenerate request that edits a stored message is rejected when it adds files beyond the limit, while regenerating a message saved before this fix still works

Because every file part is sent to the model in full, the missing limit also let one message push a very large context into a single request.

Changes

  • lib/utils/attachment-limits.ts: MAX_ATTACHMENTS_PER_MESSAGE = 3, countFileParts, remainingAttachmentSlots
  • components/chat-panel.tsx: remaining-slot check for the file picker and library attach
  • hooks/use-file-dropzone.ts: uses the shared constant and the same error message
  • app/api/chat/route.ts: server-side rejection
  • lib/streaming/helpers/prepare-messages.ts: rejects over-limit edits sent via regenerate

Test plan

  • New chat-panel tests for the file picker (partial fill and full composer) and library attach. All 3 fail against main and pass with this change
  • attachment-limits unit tests, including the guest messages path and regenerate
  • bun run test, bun lint, bun typecheck, bun format:check, bun run build

The per-message limit of 3 attachments was only enforced by the
drag-and-drop handler. The file picker capped each selection at 3 but
appended to files already in the composer, and the library picker had
no count check, so a message could carry any number of files. The chat
API did not check either.

Apply the same remaining-slot check to the file picker and library
attach, and reject submissions with more than 3 file parts in the chat
API.

Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
morphic Ready Ready Preview Sep 30, 2026 9:55pm UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T21:56:47.902462Z 6155b87 New commits
🔒 Security Review ✅ Completed 2026-09-30T21:57:33.415450Z 6155b87 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3c6306449

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/api/chat/route.ts Outdated
Comment on lines +100 to +102
const latestMessage =
message ?? (Array.isArray(messages) ? messages.at(-1) : undefined)
if (countFileParts(latestMessage) > MAX_ATTACHMENTS_PER_MESSAGE) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate the message payload actually used for guest chats

When guest chat is enabled, a caller can submit a harmless message with at most three files while placing an oversized latest user message in messages. This check always prefers message, but the guest branch later ignores it and sends messages to createEphemeralChatStreamResponse, so an unauthenticated client can bypass the server-side attachment limit and still send an arbitrarily large set of file parts to the model. Validate the payload consumed by the selected branch, or require the two representations to agree.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch. Fixed in 853a225: the check now runs on every message in messages for guests, since that is what the ephemeral path sends to the model. For authenticated users it checks message on submit-message only, so regenerating a message saved before this limit was enforced is not rejected. Covered by the new exceedsAttachmentLimit tests.

Written by an agent (Claude Code, claude-opus-5-5).

Guests send their full history in `messages` and the ephemeral path
feeds all of it to the model, so checking only `message` could be
bypassed. Check every guest message, and for authenticated users only
the submitted message so regenerating an older one keeps working.

Co-authored-by: Claude <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 853a225405

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/utils/attachment-limits.ts Outdated
Comment on lines +39 to +41
: trigger === 'submit-message'
? [message]
: []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate edited messages during regeneration

An authenticated caller can bypass the new limit by sending trigger: "regenerate-message" with an over-limit message whose ID matches messageId. This branch checks nothing, but prepareMessages treats that payload as a user-message edit, persists it with upsertMessage, and returns it to the model. Fresh evidence beyond the previously fixed guest-history path is the regeneration logic in lib/streaming/helpers/prepare-messages.ts, which uses the supplied message when the IDs match; validate that message here as well while still allowing regeneration requests that do not replace the stored message.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in d30c07c. The user-message edit path in prepareMessages now throws DeterministicPreparationError before upsertMessage when the supplied message has more than 3 file parts and more than the stored message. Plain regeneration, which resends the stored message unchanged, still works for messages saved before the limit was enforced. Covered by two new prepare-messages tests.

Written by an agent (Claude Code, claude-opus-5-5).

A regenerate request whose message id matches the stored user message
is treated as an edit, persisted, and sent to the model, so it bypassed
the submit-only check. Reject such an edit when it exceeds the limit and
carries more files than the stored message, so messages saved before the
limit was enforced can still be regenerated.

Co-authored-by: Claude <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d30c07c284

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/utils/attachment-limits.ts Outdated
Comment on lines +39 to +41
: trigger === 'submit-message'
? [message]
: []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate attachments for unknown submission triggers

When an authenticated client—or a deployment using ENABLE_AUTH=false—omits trigger or supplies an unrecognized value, this selects an empty list even if message contains more than three files. The route does not reject unknown triggers, and prepareMessages treats every non-regeneration trigger as a normal submission, so the oversized message is persisted and sent to the model. Reject unknown trigger values in the request schema, or validate message for every non-regeneration path.

AGENTS.md reference: AGENTS.md:L130-L130

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 6155b87. For authenticated requests the check now applies to message on every trigger except regenerate-message, matching how prepareMessages treats every other trigger as a submission. Regenerate stays covered by the stored-message comparison in prepareMessages. Added a test for a missing and an unknown trigger.

Written by an agent (Claude Code, claude-opus-5-5).

prepareMessages treats any trigger other than regenerate-message as a
submission, so a missing or unknown trigger skipped the check while
the message was still persisted and sent to the model.

Co-authored-by: Claude <noreply@anthropic.com>
@miurla
miurla merged commit 653f174 into main Sep 30, 2026
7 checks passed
@miurla
miurla deleted the fix/enforce-attachment-limit branch September 30, 2026 22:04

This branch was successfully deployed

1 active deployment
Preview — 6155b877 Deployed Sep 30, 2026 by vercel[bot]
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