Describe the bug
When resuming a previous chat thread, cl.Pdf elements re-sent to restore the PDF side panel render completely blank. The file is fetched successfully (HTTP 200) but the browser refuses to display it inside the iframe because Azure Blob Storage returns Content-Disposition: attachment instead of Content-Disposition: inline.
This is specific to chat resume: on a fresh session the element may appear to work because the URL hasn't been resolved through the data layer yet, but on resume the persisted Azure Blob URL is used directly and the Content-Disposition header blocks inline rendering every time.
The root cause is in SQLAlchemyDataLayer.create_element (chainlit/data/sql_alchemy.py):
uploaded_file = await self.storage_provider.upload_file(
object_key=file_object_key, data=content, mime=element.mime, overwrite=True
# ↑ content_disposition is never passed — Azure defaults to "attachment"
)
AzureStorageClient.upload_file accepts a content_disposition parameter and passes it to ContentSettings, but the caller never provides it.
To Reproduce
- Configure a Chainlit app with the SQLAlchemy data layer and an Azure Blob Storage provider (
BUCKET_NAME, APP_AZURE_STORAGE_ACCOUNT, APP_AZURE_STORAGE_ACCESS_KEY set).
- Start a new chat session and send a
cl.Pdf element with a local path=:
pdf = cl.Pdf(name="contract.pdf", path="/tmp/contract.pdf", display="side")
await cl.Message(content="Here is the PDF:", elements=[pdf]).send()
- Close the chat and reopen it from the thread history (triggering
on_chat_resume).
- The
cl.Pdf element is re-sent during resume to restore the side panel.
- Click the PDF element in the chat UI.
- Observe the PDF panel is blank / white.
Expected behavior
The PDF should render inline in the side panel, as it does when no data layer is configured (Chainlit serves the file itself without a Content-Disposition header).
Screenshots
Network inspector showing the Azure Blob response when the PDF element is clicked:
URL: https://<account>.blob.core.windows.net/<container>/threads/<id>/files/<id>?st=...
Status: 200 OK
Response Headers:
Content-Disposition: attachment; filename="contract.pdf"
Content-Type: application/pdf
The side panel is completely blank despite the 200 response.
Desktop (please complete the following information):
- OS: macOS 15 (Sequoia)
- Browser: Safari 26.4, Chrome 124
- Version: Chainlit 2.9.4
Additional context
- This reproduces consistently on chat resume (
on_chat_resume). On a brand-new session the element is sometimes rendered before the blob URL is resolved, but on resume the stored Azure Blob URL is used immediately and the Content-Disposition: attachment header always blocks inline rendering.
- This only affects deployments using Azure Blob Storage as the data layer backend. Local file serving (no data layer) is unaffected.
- The
AzureStorageClient.upload_file signature already supports content_disposition: str | None = None — the fix is a one-liner in the calling code.
Suggested fix
In SQLAlchemyDataLayer.create_element, pass content_disposition="inline" for MIME types that browsers can render:
_INLINE_MIME_TYPES = {
"application/pdf",
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
}
content_disposition = "inline" if element.mime in _INLINE_MIME_TYPES else None
uploaded_file = await self.storage_provider.upload_file(
object_key=file_object_key,
data=content,
mime=element.mime,
overwrite=True,
content_disposition=content_disposition,
)
Workaround
Pass url= directly to cl.Pdf pointing to a locally served static endpoint so Chainlit skips the data-layer upload entirely:
pdf = cl.Pdf(name="contract.pdf", url="http://localhost:8000/static/contract.pdf", display="side")
Describe the bug
When resuming a previous chat thread,
cl.Pdfelements re-sent to restore the PDF side panel render completely blank. The file is fetched successfully (HTTP 200) but the browser refuses to display it inside the iframe because Azure Blob Storage returnsContent-Disposition: attachmentinstead ofContent-Disposition: inline.This is specific to chat resume: on a fresh session the element may appear to work because the URL hasn't been resolved through the data layer yet, but on resume the persisted Azure Blob URL is used directly and the
Content-Dispositionheader blocks inline rendering every time.The root cause is in
SQLAlchemyDataLayer.create_element(chainlit/data/sql_alchemy.py):AzureStorageClient.upload_fileaccepts acontent_dispositionparameter and passes it toContentSettings, but the caller never provides it.To Reproduce
BUCKET_NAME,APP_AZURE_STORAGE_ACCOUNT,APP_AZURE_STORAGE_ACCESS_KEYset).cl.Pdfelement with a localpath=:on_chat_resume).cl.Pdfelement is re-sent during resume to restore the side panel.Expected behavior
The PDF should render inline in the side panel, as it does when no data layer is configured (Chainlit serves the file itself without a
Content-Dispositionheader).Screenshots
Network inspector showing the Azure Blob response when the PDF element is clicked:
The side panel is completely blank despite the 200 response.
Desktop (please complete the following information):
Additional context
on_chat_resume). On a brand-new session the element is sometimes rendered before the blob URL is resolved, but on resume the stored Azure Blob URL is used immediately and theContent-Disposition: attachmentheader always blocks inline rendering.AzureStorageClient.upload_filesignature already supportscontent_disposition: str | None = None— the fix is a one-liner in the calling code.Suggested fix
In
SQLAlchemyDataLayer.create_element, passcontent_disposition="inline"for MIME types that browsers can render:Workaround
Pass
url=directly tocl.Pdfpointing to a locally served static endpoint so Chainlit skips the data-layer upload entirely: