Skip to content

download_files fails for non-ASCII filenames with python-multipart >= 0.0.30 #5125

Description

@jihsheng-huang

Summary

download_files (both AsyncDaytona and Daytona) fails to download any file whose remote path contains non-ASCII characters when the runtime has python-multipart >= 0.0.30 installed. With python-multipart <= 0.0.29 the same download succeeds. Uploading non-ASCII filenames is unaffected — only download breaks.

The failure surfaces as, e.g.:

DaytonaError: Failed to download file: Failed to download files: '/tmp/___.txt'

where ___ is the ASCII-fallback rendering of the original non-ASCII name.

Environment

  • daytona Python SDK: reproduced on 0.187.0; the relevant parse code is unchanged through latest 0.190.1.
  • python-multipart: >= 0.0.30 broken, <= 0.0.29 works. (It's a transitive dependency that downstream users routinely upgrade for security fixes — e.g. the high-severity CVE-2026-53539, fixed in 0.0.30.)
  • Python 3.12

What happens

The download response is multipart/form-data, and each part identifies its file via the Content-Disposition header. Captured from a download response for /tmp/あああ.txt:

Content-Disposition: form-data; name="file"; filename="/tmp/___.txt"; filename*=utf-8''%2Ftmp%2F%E3%81%82%E3%81%82%E3%81%82.txt
  • filename = /tmp/___.txt — an ASCII-only fallback (non-ASCII chars dropped or replaced with _)
  • filename* = percent-encoded UTF-8 = the real /tmp/あああ.txt

The SDK correlates each response part back to the request by key-matching on the path, and that key is the bare filename — so the requested path (written as the key) and the parsed path (read back as the key) must be identical:

  • daytona/_async/filesystem.py:412src_file_meta_dict[f.source] = ... — builds the request map keyed by the requested path = /tmp/あああ.txt
  • daytona/_async/filesystem.py:503source = cd_params.get(b"filename", ...) — reads the part's bare filename. With filename* ignored, this is the ASCII fallback with non-ASCII chars dropped = /tmp/___.txt, not the requested path
  • daytona/_async/filesystem.py:512meta = src_file_meta_dict[source] — looks the part up by that key → KeyError('/tmp/___.txt'), because /tmp/___.txt was never a key (the map holds /tmp/あああ.txt)
  • daytona/_sync/filesystem.py — same pattern; filename* is read nowhere in the SDK (through 0.190.1).

python-multipart 0.0.30 deliberately stopped exposing filename* (#291), since RFC 7578 §4.2 forbids it in multipart/form-data — so the bare ASCII filename is all the SDK can read.

Reproduction

A minimal script — runs as-is with daytona and python-multipart >= 0.0.30 installed and DAYTONA_API_KEY set in the environment:

import asyncio

from daytona import AsyncDaytona

FILENAMES = [
    '/tmp/happy_path.txt',  # ASCII
    '/tmp/café.txt',  # Latin-1 accented (single non-ASCII char)
    '/tmp/あああ.txt',  # Japanese
]
CONTENT = b'roundtrip-content'


async def main() -> int:
    success_count = 0
    async with AsyncDaytona() as daytona:
        sandbox = await daytona.create(timeout=120)
        try:
            for path in FILENAMES:
                # Upload always works; download is the failing direction.
                await sandbox.fs.upload_file(CONTENT, path)
                try:
                    got = await sandbox.fs.download_file(path)
                    if got == CONTENT:
                        success_count += 1
                        status = 'PASS'
                    else:
                        status = f'FAIL (content mismatch: {got!r})'
                except Exception as e:
                    status = f'FAIL ({type(e).__name__}: {e})'
                print(f'{path:30}  {status}')
        finally:
            await sandbox.delete()
    print(f'{success_count}/{len(FILENAMES)} downloaded successfully')


if __name__ == '__main__':
    asyncio.run(main())

Output with python-multipart >= 0.0.30 (only the ASCII name survives):

/tmp/happy_path.txt             PASS
/tmp/café.txt                   FAIL (DaytonaError: Failed to download file: Failed to download files: '/tmp/caf.txt')
/tmp/あああ.txt                  FAIL (DaytonaError: Failed to download file: Failed to download files: '/tmp/___.txt')
1/3 downloaded successfully

With python-multipart <= 0.0.29 all three pass. Uploading the same names always succeeds — only download breaks.

Affects all non-ASCII filenames (not script-specific)

Swept across scripts; every non-ASCII name fails because the bare filename is an ASCII-only fallback while the real name lives only in the now-ignored filename*:

requested filename bare filename seen by SDK result
café.txt caf.txt (é dropped) FAIL
あああ.txt ___.txt FAIL
안녕.txt __.txt FAIL
中文.txt __.txt FAIL
Привет.txt ______.txt FAIL
Ελληνικά.txt ________.txt FAIL

café.txt (a single Latin-1 character) fails too, so this is any non-ASCII filename, not a CJK/multibyte-specific issue.

Expected behavior

Downloading a file with a non-ASCII path should succeed regardless of the installed python-multipart version. (Uploading such files already works, so the round-trip is currently asymmetric.) We're not sure of the best place to fix this given the SDK ↔ daemon split, so we'll leave the approach to the maintainers — the key observation is just that the SDK currently relies on the bare filename, while the only faithful copy of the name is in filename*, which python-multipart >= 0.0.30 no longer exposes.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions