Skip to content

cl.Pdf element blank on chat resume with Azure Blob storage — Content-Disposition: attachment #2946

Description

@koulakhilesh

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

  1. 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).
  2. 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()
  3. Close the chat and reopen it from the thread history (triggering on_chat_resume).
  4. The cl.Pdf element is re-sent during resume to restore the side panel.
  5. Click the PDF element in the chat UI.
  6. 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")

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendPertains to the Python backend.bugSomething isn't workingdata layerPertains to data layers.needs-triagestaleIssue has not had recent activity or appears to be solved. Stale issues will be automatically closed

    Type

    No fields configured for Bug.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions