What happened?
With fileStrategy: azure_blob and a private blob container, every read of an uploaded file fails. Uploads succeed, so the blobs are all there — they just cannot be read back.
The visible symptom is a provider error that points nowhere near the cause. Pasting an image into a chat ends the turn with:
400 litellm.BadRequestError: BadRequestError - Failed to download image from
https://<account>.blob.core.windows.net/<container>/images/<userId>/<uuid>__image.png
and the server log right before it says:
[getAzureFileStream] Error getting blob stream: Request failed with status code 409
Error processing image from blob storage: Request failed with status code 409
(409 PublicAccessNotPermitted is what a storage account with allowBlobPublicAccess: false returns; a container whose access level is private returns 404 instead. Either way the read fails.)
Root cause
Uploads and downloads take different code paths, and only the upload authenticates.
saveBufferToAzure uses the SDK client built from AZURE_STORAGE_CONNECTION_STRING (or Managed Identity) — api/server/services/Files/Azure/crud.js.
getAzureFileStream does a plain, credential-free HTTP GET on the blob URL:
async function getAzureFileStream(_req, fileURL) {
const response = await axios({ method: 'get', url: fileURL, responseType: 'stream' });
return response.data;
}
That only works when the container allows anonymous access. AZURE_STORAGE_PUBLIC_ACCESS defaults to 'true' in the same file, so the happy path is a public container, and a private one is broken everywhere getDownloadStream is used:
- vision (
encodeAndFormat)
/api/files/:file_id/preview and /api/files/download/...
- image tools (
OpenAIImageTools.js)
- code-interpreter re-priming
The failure is then downgraded rather than surfaced: encodeAndFormat catches it, logs, and falls through to prepareImagePayload, which for Azure returns the raw blob URL. That URL is sent to the provider, the provider cannot read it either, and the user sees a provider 400 instead of a storage error.
Second, related gap: the Azure strategy has no getDownloadURL, while S3 has a presigned one (api/server/routes/files/files.js). So /api/files/download-url/... answers 501 Not Implemented for Azure and the client falls back to the stream that just failed. There is no SAS code anywhere in the repository, so a private container has no working read path at all.
Steps to reproduce
- Create a storage account with
allowBlobPublicAccess: false (or a container with private access level).
- Set
fileStrategy: azure_blob and a valid AZURE_STORAGE_CONNECTION_STRING, with AZURE_STORAGE_PUBLIC_ACCESS=false.
- Upload or paste an image and send it to any model.
Expected: the image is read back and sent to the model.
Actual: the read fails, the raw blob URL goes to the provider, and the turn dies with a provider "failed to download" 400. The image tile does not render either, because the client puts the same URL into <img src>.
What should happen
azure_blob should work against a private container, which is the configuration most Azure tenants require:
getAzureFileStream should download through the authenticated client.
- The strategy should implement
getDownloadURL with a short-lived read-only SAS URL, matching what S3 already does.
I have a PR ready for both.
Version Information
Reproduced on v0.8.8-rc1; the code path is unchanged on main.
Additional context
One detail worth keeping in a SAS implementation: Azure validates the SAS st/se window against its own clock, so a token minted with startsOn = now is rejected with "Signature not valid in the specified time frame" whenever the host clock runs slightly ahead. Backdating the start by a few minutes avoids a class of intermittent 403s. (Chainlit hits the same trap with generate_blob_sas.)
What happened?
With
fileStrategy: azure_bloband a private blob container, every read of an uploaded file fails. Uploads succeed, so the blobs are all there — they just cannot be read back.The visible symptom is a provider error that points nowhere near the cause. Pasting an image into a chat ends the turn with:
and the server log right before it says:
(409
PublicAccessNotPermittedis what a storage account withallowBlobPublicAccess: falsereturns; a container whose access level is private returns 404 instead. Either way the read fails.)Root cause
Uploads and downloads take different code paths, and only the upload authenticates.
saveBufferToAzureuses the SDK client built fromAZURE_STORAGE_CONNECTION_STRING(or Managed Identity) —api/server/services/Files/Azure/crud.js.getAzureFileStreamdoes a plain, credential-free HTTP GET on the blob URL:That only works when the container allows anonymous access.
AZURE_STORAGE_PUBLIC_ACCESSdefaults to'true'in the same file, so the happy path is a public container, and a private one is broken everywheregetDownloadStreamis used:encodeAndFormat)/api/files/:file_id/previewand/api/files/download/...OpenAIImageTools.js)The failure is then downgraded rather than surfaced:
encodeAndFormatcatches it, logs, and falls through toprepareImagePayload, which for Azure returns the raw blob URL. That URL is sent to the provider, the provider cannot read it either, and the user sees a provider 400 instead of a storage error.Second, related gap: the Azure strategy has no
getDownloadURL, while S3 has a presigned one (api/server/routes/files/files.js). So/api/files/download-url/...answers501 Not Implementedfor Azure and the client falls back to the stream that just failed. There is no SAS code anywhere in the repository, so a private container has no working read path at all.Steps to reproduce
allowBlobPublicAccess: false(or a container with private access level).fileStrategy: azure_bloband a validAZURE_STORAGE_CONNECTION_STRING, withAZURE_STORAGE_PUBLIC_ACCESS=false.Expected: the image is read back and sent to the model.
Actual: the read fails, the raw blob URL goes to the provider, and the turn dies with a provider "failed to download" 400. The image tile does not render either, because the client puts the same URL into
<img src>.What should happen
azure_blobshould work against a private container, which is the configuration most Azure tenants require:getAzureFileStreamshould download through the authenticated client.getDownloadURLwith a short-lived read-only SAS URL, matching what S3 already does.I have a PR ready for both.
Version Information
Reproduced on
v0.8.8-rc1; the code path is unchanged onmain.Additional context
One detail worth keeping in a SAS implementation: Azure validates the SAS
st/sewindow against its own clock, so a token minted withstartsOn = nowis rejected with "Signature not valid in the specified time frame" whenever the host clock runs slightly ahead. Backdating the start by a few minutes avoids a class of intermittent 403s. (Chainlit hits the same trap withgenerate_blob_sas.)