Skip to content

fix: Azure Blob (private container) image support: authenticated vision downloads + UI display - #13411

Open
rahepler2 wants to merge 8 commits into
LibreChat-AI:devfrom
rahepler2:azure-blob-fix
Open

rahepler2 wants to merge 8 commits into
LibreChat-AI:devfrom
rahepler2:azure-blob-fix

Conversation

@rahepler2

Copy link
Copy Markdown

Summary

Makes Azure Blob storage work end-to-end with a private container
(AZURE_STORAGE_PUBLIC_ACCESS=false). Previously, images only worked if the
container was left publicly readable, which isn't acceptable for enterprise /
regulated / sovereign-cloud deployments.

With a private container, two things were broken:

  1. **Vision requests: ** the image was handed to the LLM as a raw
    https://<account>.blob.core.windows.net/... URL that remote providers can't
    fetch. getAzureFileStream used an anonymous HTTP GET, and encodeAndFormat
    silently fell back to that unfetchable URL.
  2. UI display: the chat <img> pointed at the same private URL; an <img>
    tag fires an unauthenticated request (it can't attach the server's Managed
    Identity or the LibreChat JWT), so the browser got a 403.

Changes

  • Backend (vision): getAzureFileStream now downloads via the authenticated
    Azure SDK (Managed Identity / DefaultAzureCredential), matching the existing
    deleteFileFromAzure pattern. encodeAndFormat now fails closed for
    azure_blob (throws a clear error) instead of falling back to a private URL.
    The fail-closed is scoped to azure_blob only, so S3 / Firebase / CloudFront
    keep their existing fallback to their fetchable URLs on a download error.
  • Frontend (display): a reusable ImageFile component renders the image from
    its direct URL first and, only if that load fails, fetches the bytes through
    the existing authenticated /files/download proxy (which streams using the
    server's credentials) and renders them from a local blob: URL. Public Azure
    containers continue to load directly; private containers fall back to the proxy.
    Other sources render their URL directly, unchanged.
  • Sovereign clouds: storage endpoint suffix is configurable via
    AZURE_STORAGE_ENDPOINT_SUFFIX (defaults to core.windows.net); documented in
    .env.example.

No SAS tokens and no new RBAC role — the existing Storage Blob Data Reader on
the app's identity covers both the vision and display paths. Public-container
behavior is unchanged.

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Testing

Configure LibreChat with Azure Blob storage using Managed Identity and a
private container (AZURE_STORAGE_PUBLIC_ACCESS=false). Grant the app's
identity Storage Blob Data Reader on the storage account.

  1. Upload an image and send it to a vision model.
    • Before: model can't see the image / request fails; outbound payload
      contains a *.blob.core.* URL.
    • After: outbound payload contains data:image/...;base64,... and the
      model responds correctly.
  2. Reload the conversation (clears the in-session preview).
    • Before: image is broken in the UI (403).
    • After: image renders (direct load 403s, then falls back to the
      authenticated /files/download proxy as a blob: URL).
  3. Public container: confirm images still load directly (no proxy round-trip).
  4. Sovereign cloud: set AZURE_STORAGE_ENDPOINT_SUFFIX=core.usgovcloudapi.net
    and confirm the blob service URL uses that suffix.

Automated tests added/updated:

  • api/.../Azure/__tests__/crud.test.js — SDK download path parsing & failure modes.
  • api/.../images/__tests__/encode.test.js — encodeAndFormat fails closed for azure_blob.
  • packages/api/src/cdn/__tests__/azure.test.ts — endpoint suffix default + override.
  • client/.../Content/__tests__/ImageFile.test.tsx — direct render, proxy-on-error, local preview.
  • client/.../Files/__tests__/previewRefetchInterval.spec.ts — isProxyImageSource.

npm run test:client (2279 tests) passes; backend tests for the changed files
pass. Note: the unrelated server/routes/agents/__tests__/responses.spec.js
integration suite fails locally without LLM provider credentials and is not
affected by this change.

Test Configuration:

  • Azure Blob storage, Managed Identity (DefaultAzureCredential), private
    container, Azure Container Apps. Node 20.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • I have made pertinent documentation changes
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective
  • Local unit tests pass with my changes

@rahepler2

rahepler2 commented May 31, 2026 •

Copy link
Copy Markdown
Author

@danny-avila when you review this, I did see a PR for a similar approach. (#13245).

The core difference is, mine authenticates Blob reads with the Container App's managed identity and needs only Storage Blob Data Reader, and not Storage Blob Delegator, account key, and signed URLs.

They don't need to be mutually exclusive depending on which path you'd prefer. Even if the SAS is preferred the fall back for soveriegn clouds that cannot allow delegation could use the path I have chosen.

@JorgeCosta87

Copy link
Copy Markdown
Contributor

Thanks for picking this up, @rahepler2 — agreed these aren't mutually exclusive, and I think they slot together cleanly. Sketching the two access models so @danny-avila has the full picture:

The one genuinely shared piece is the server-side getAzureFileStream fix — switching it from the unauthenticated axios.get to the authenticated SDK is union-correct: it strips any SAS and reads via the server credential, so it's right under both models (and is exactly what #13340 calls out as the S3-vs-Azure inconsistency). I'm happy to host that + the encodeAndFormat fail-closed inside #13245 since it already rewrites that Azure-crud surface — which would leave #13411 owning the part #13245 doesn't have: the frontend ImageFile proxy-fallback for display. Or if you'd rather keep the backend fix here, that's fine too — whatever's easiest for @danny-avila to merge.

Two small notes: #13245 already implements the Managed Identity path (so it also covers #10784), and it handles the sovereign-cloud endpoint via a full-URL AZURE_STORAGE_BLOB_ENDPOINT override — might be worth consolidating on one env var rather than also adding AZURE_STORAGE_ENDPOINT_SUFFIX.

@rahepler2

rahepler2 commented Jun 1, 2026 •

Copy link
Copy Markdown
Author

@JorgeCosta87 Hey, thanks for writing up the update!

I did realize in testing there were a few more routes that needed to be covered by this same azure blob MI and SAS approach. Look at the latest commit to this branch I just pushed, but the proxy for agent, tool, and generated images were also lacking the necessary support.

So for exampled, if I call my MCP tool to get an image back of something, that would use the route to get it into blob storage correctly, but wasn't able to display it. Updated those accordingly, but will need it on your side (if you haven't already done so).

@rahepler2

Copy link
Copy Markdown
Author

Pushed a fix for the failing test:ci job. The two failures (ArtifactRouting, OpenAIImageGen) were because those suites render the converted components in isolation without the app providers, and the new ImageFile component uses Recoil + React Query. I mocked ImageFile in both suites with the same leaf-mock pattern they already use for Image. The assertions are unchanged. Full client suite passes locally.

@rahepler2

Copy link
Copy Markdown
Author

@danny-avila Wanted to see about review for this.

@rahepler2

Copy link
Copy Markdown
Author

@JorgeCosta87

removed the added env on mine and adjusted to use AZURE_STORAGE_BLOB_ENDPOINT

@rahepler2

Copy link
Copy Markdown
Author

Hey @danny-avila, just wanted to bump this and see about review. We are looking to move towards larger testing and would need this in place.

@rahepler2
rahepler2 force-pushed the azure-blob-fix branch 2 times, most recently from f2ed405 to 9624b57 Compare July 21, 2026 12:47
@kayahk

kayahk commented Aug 10, 2026

Copy link
Copy Markdown

IMHO, this is currently a huge blocker for Azure users who want/need to enable secure file upload and storage in the private containers. Any timeline when this will be moved forward? Thanks.

@rahepler2

Copy link
Copy Markdown
Author

IMHO, this is currently a huge blocker for Azure users who want/need to enable secure file upload and storage in the private containers. Any timeline when this will be moved forward? Thanks.

Waiting for review and approval from maintainers on this

@codegraph-librechat codegraph-librechat Bot added the 🗺️ File Storage codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 1, 2026
@github-actions
github-actions Bot changed the base branch from main to dev October 2, 2026 20:13
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for the contribution! LibreChat merges all changes into dev first — main only moves at release time — so this pull request's base branch was switched from main to dev automatically.

Nothing is needed from you; your commits, reviews and discussion are unchanged. If the diff now shows files you did not touch, rebase onto dev:

git remote add upstream https://github.com/LibreChat-AI/LibreChat.git
git fetch upstream dev
git rebase upstream/dev
git push --force-with-lease

Maintainers: apply the target: main label and restore the base branch if this one genuinely belongs on main.

@codegraph-librechat codegraph-librechat Bot added the 🗺️ Server Core codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 3, 2026
Replace anonymous HTTP blob fetches in getAzureFileStream with the
SDK-authenticated container client so private/non-public containers
(default in regulated environments, only option in GCC High) work for
vision payloads instead of returning 403.

Fail closed in encodeAndFormat when a blob download fails rather than
silently falling back to a URL the remote LLM cannot fetch, and
parameterize the storage endpoint suffix for sovereign clouds via
AZURE_STORAGE_ENDPOINT_SUFFIX.
Chat images from object-storage sources that require server-side auth (e.g.
private Azure Blob) cannot be loaded by an <img> tag, which fires an
unauthenticated request the browser cannot sign. Such images previously
rendered only when the container allowed anonymous public access.

Add a reusable ImageFile component that, for these sources, fetches the bytes
through the existing authenticated /files/download proxy (which streams using
the server's credentials) and renders them from a local blob: URL. All other
sources continue to render their URL directly. Used by both the message
content (Part) and attachment (Files) render paths.
Limit the encodeAndFormat fail-closed throw to azure_blob so S3, Firebase,
and CloudFront keep falling back to their fetchable URLs on a download error.

Render proxy-eligible images (private Azure Blob) from their direct URL first
and fetch through the authenticated download proxy only when the direct load
fails, so public Azure containers continue to load images directly instead of
routing every image through the backend.
Route the tool-output (LogContent), attachment (Attachment), and
image-generation (OpenAIImageGen) render paths through ImageFile so images
from auth-required sources (private Azure Blob) load via the authenticated
download proxy instead of an anonymous <img> request that a private container
rejects. Add an optional args passthrough to ImageFile for the
image-generation dialog metadata.
Drop AZURE_STORAGE_ENDPOINT_SUFFIX (env var, azure.ts handling, .env.example
docs, and its test) to consolidate on LibreChat-AI#13245's AZURE_STORAGE_BLOB_ENDPOINT
full-URL override, per review discussion. The backend authenticated download
and the frontend ImageFile display proxy are unchanged.
SteerPart (new on dev) rendered attached images with Image directly, so
private Azure Blob images on steer messages failed to load. Render them
through ImageFile, keeping SteerPart's size and alt-text fallbacks.

Wrap Files and Part image tests in Recoil/React Query providers, which
ImageFile needs, and apply prettier/import sorting to touched files.
@rahepler2

rahepler2 commented Oct 10, 2026 •

Copy link
Copy Markdown
Author

@danny-avila Wanted to check back in on this. I'm seeing more issues pop up around this and more PR's coming in.

Is there something your'd like me to do to organize things further between Jorge and my original PR?

@codegraph-librechat codegraph-librechat Bot added the 🗺️ Core Infra & Sec codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 15, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Core Infra & Sec codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ File Storage codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Platform Security codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Server Core codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants