Skip to content

[Bug]: S3 presigned URLs in messages.attachments / messages.files / toolcalls.attachments expire on read (images 403 after S3_URL_EXPIRY_SECONDS) #13751

Closed

Description

@Falenos

What happened?

S3 presigned URLs embedded in persisted message and tool-call snapshots are never refreshed on read. Once a presigned URL expires (default S3_URL_EXPIRY_SECONDS=120, i.e. 2 minutes), any image or file rendered from those snapshots returns 403 SignatureDoesNotMatch and shows as a broken tile in the UI.

User-facing symptom: a user generates an image (or uploads one) in a conversation, leaves it, and on return — or on initial load of any older conversation — the image is gone.

The bug affects three persistence sites that all snapshot the full file record (including the SAS-signed filepath) into denormalized [Mixed] arrays that are never updated after write:

  • messages.attachments[] — tool outputs from image_gen_oai, DALL·E, Flux, Stable Diffusion, code execution, etc. (written by api/server/controllers/agents/callbacks.js Tools_End handler via saveBase64Image).
  • messages.files[] — user-uploaded files referenced in a message (written by buildMessageFiles in packages/api/src/utils/message.ts).
  • toolcalls.attachments[] — code-interpreter / execute_code artifacts (packages/data-schemas/src/schema/toolCall.ts:10,40), rehydrated by the client through useToolCallsMap on conversation open.

These snapshots are served verbatim by the read endpoints below. The existing refreshS3FileUrls chain (packages/api/src/storage/s3/crud.ts:936) is wired only into GET /api/files and the avatar dispatchers — it never touches messages or tool calls.

Root cause: the following endpoints return persisted documents without re-signing the embedded URLs:

  • routes/messages.js — GET / (cursor + search branches), GET /:conversationId, GET /:conversationId/:messageId (3 sites).
  • routes/share.js — GET /:shareId (getSharedMessages response).
  • controllers/tools.js — GET /api/agents/tools/calls (getToolCalls).

I have a fix prepared — a small refresh-on-read shim that walks attachments[]/files[] on the response payload and dispatches to the existing refreshS3Url per entry. In-memory only (no Mongo writes), per-call memoization keyed on filepath, errors retain the original URL. Happy to open a PR against dev if maintainers agree on the approach.

Version Information

Reproducible on current main and dev (verified against dev at commit 9efe4878e, "🅰️ feat: Native Anthropic Provider for Custom Endpoints").

The bug exists in any release with fileStrategy: "s3" since refreshS3FileUrls was introduced; nothing has wired it into the message or toolcall read paths. Reproducible in self-hosted Docker, source builds, and managed deployments alike.

Steps to Reproduce

  1. Configure .env with fileStrategy=s3, valid S3 creds, and S3_URL_EXPIRY_SECONDS=30 (any short TTL; default 120 also reproduces, just takes longer).
  2. Configure an agent with image_gen_oai available, or use any default chat with image-gen enabled.
  3. Send a message that triggers image generation. Confirm the image renders.
  4. Wait > S3_URL_EXPIRY_SECONDS (e.g. 35 seconds with the setting above).
  5. Reload the conversation, or navigate away and back, or open the conversation from a fresh tab.
  6. Observed: the image tile renders as broken. Browser network panel shows 403 SignatureDoesNotMatch from s3.amazonaws.com (or your S3-compatible endpoint).
  7. Expected: the image should still render. The blob in S3 is intact; only the URL signature expired.

Same repro applies to:

  • User-uploaded images in messages.files[] (upload an image to a message, send, wait, reload).
  • Code-interpreter outputs in toolcalls.attachments[] (run an execute_code tool that produces a chart, wait, reload — the chart 403s when useToolCallsMap rehydrates it).
  • Shared conversations carrying any of the above (anonymizeMessages in packages/data-schemas/src/methods/share.ts:60 copies the snapshot at share creation, then GET /api/share/:shareId returns it verbatim).

What browsers are you seeing the problem on?

Chrome, Firefox, Safari, Edge — backend issue, browser-independent.

Relevant log output

# Browser DevTools → Network on the image request:
GET https://<bucket>.s3.<region>.amazonaws.com/<basePath>/<userId>/<file>?...&X-Amz-Date=20260615T...&X-Amz-Expires=30&...
→ 403 Forbidden
<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>AccessDenied</Code>
  <Message>Request has expired</Message>
  <X-Amz-Expires>30</X-Amz-Expires>
  ...
</Error>

# Backend has no log for this — LibreChat is not in the request path. The
# signed URL was served from MongoDB on the prior GET /api/messages call;
# S3 enforces expiry directly against the client fetch.

Screenshots

n/a — broken image tile in conversation history. Happy to attach if useful.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Additional context

Why this matters at default settings: DEFAULT_EXPIRY_SECONDS = 2 * 60 in packages/api/src/storage/s3/s3Config.ts. Any user opening a conversation older than two minutes hits this on every image in that conversation.

Adjacent open work: #13740 (Snapshot Files for Shared-Link Attachments) introduces a fileSnapshots[] model on SharedLink and applies a read-time URL rewrite in getSharedMessages for share-scoped serving. That work targets a different concern (auth boundaries for anonymous viewers), but it shares the "rewrite URLs on the response" pattern — so coordinating on routes/share.js would avoid conflicts when both land.

Activity

  1. Falenos commented on Jun 15, 2026

    @Falenos
    ContributorAuthor

    Adding cross-links to prior user reports of this same bug for searchability:

    Adjacent (fixed already): #11445 — that one was the agent-avatar-only slice and was resolved by the existing refreshS3FileUrls chain, but the message/toolcall snapshot sites were never wired up.

  2. locked and limited conversation to collaborators on Jun 15, 2026
  3. converted this issue into a discussion #13762 on Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions