Skip to content

Overview

browser-download-filename names downloaded files the way Chrome does. It is a pure-Python port of two parts of Chromium, the open-source core of Chrome, Edge and other browsers: the Content-Disposition header parser (net::HttpContentDisposition) and the function that picks a download's file name from the response (net::GetSuggestedFilename).

Quick start

python -m pip install browser-download-filename
>>> from browser_download_filename import suggested_filename
>>> header = b"attachment; filename=report.pdf; filename*=UTF-8''%E5%A0%B1%E5%91%8A.pdf"
>>> headers = [("Content-Disposition", header)]
>>> suggested_filename(headers, url="https://example.com/download?id=42")
'報告.pdf'

The result is a safe file name to save under, and the name Chrome reports to DevTools and Playwright as the download's suggested file name, except in the cases listed under Limits of the port. See Usage for passing the headers from httpx2, httpx, requests, aiohttp and urllib.request.

Why

When a program downloads a file with an HTTP library, picking the file name is up to the program. The server's suggestion is in the Content-Disposition header, and servers write it in many ways that the standards do not allow but browsers accept. Code that reads the header by the standards gets those names wrong; the standard library's email module, for example:

Content-Disposition Chrome and this package email.message.Message.get_filename()
attachment; filename="fallback.zip"; filename*=UTF-8''%E4%B8%AD%E6%96%87.zip 中文.zip fallback.zip
attachment; filename="%E4%B8%AD%E6%96%87.zip" 中文.zip %E4%B8%AD%E6%96%87.zip
attachment; filename="=?UTF-8?B?5Lit5paHLnppcA==?=" 中文.zip =?UTF-8?B?5Lit5paHLnppcA==?=
attachment; filename="中文.zip" in raw UTF-8, read with requests 中文.zip 'ä¸\xadæ\x96\x87.zip'

No standard says what to do with such headers either: the HTML standard defers to RFC 6266, and the web-platform-tests have no test of file name parsing. Browsers agree on well-formed headers and differ on the rest; Chrome is the most used of them, so it is the reference this package follows. Behavior and browser differences shows where Firefox and Safari differ.

The name in the header is only the start. Chrome falls back to the URL and then to download, and it sanitizes whatever it picked: path separators, control characters and other characters that file systems reject are replaced, and leading and trailing dots are removed, so ../../etc/passwd becomes _.._etc_passwd. suggested_filename does all of it.

What you get

Function What it does
suggested_filename(headers, url=..., ...) The file name Chrome picks for a download: from Content-Disposition, an <a download> value, the URL or a default, then sanitized; a name that is not from Content-Disposition can get the extension of the MIME type. Content-Disposition and the MIME type come from the response's headers, or from content_disposition= and content_type=; platform= chooses between Chrome's Windows and POSIX rules.
sanitize_filename(name) Chrome's sanitization applied to a name from anywhere, such as a page title or user input, the way Chrome cleans an <a download> value.
parse_content_disposition(value) The disposition type and the decoded file name of a header, as Chromium reads them, without sanitizing, and whether Chrome refuses the response because its Content-Disposition values differ or its headers hold a NUL.

suggested_filename and parse_content_disposition take a response's headers, or the header alone as bytes or as a str, and never raise on a malformed header. The package has no dependencies outside the standard library and supports CPython 3.11 and later, and PyPy.

How exact it is

The parser, the charset decoders and the file name rules are ports of Chromium's code at a pinned revision, and Chromium's own unit test cases are part of the test suite. CI also downloads more than 900 cases with a real Chromium and checks every result; How it is checked describes the tests. Behavior and browser differences lists what Chrome does that this package leaves to you, such as numbering files that already exist, and, under Limits of the port, where the package's name can differ from Chrome's.

Next: examples are in Usage; the full API is in API reference.