ankitects/anki · error · ValueError

Unexpected content-disposition header: {resp.headers.get('co

Error message

Unexpected content-disposition header: {resp.headers.get('content-disposition')}

What it means

download_addon() in qt/aqt/addons.py fetches an add-on package from AnkiWeb and derives the package filename from the HTTP `content-disposition` response header, expecting the exact form `attachment; filename=<name>`. When the header is missing entirely (KeyError on direct dict access) or does not match the regex `attachment; filename=(.+)`, a ValueError with the unexpected header value is raised. The exception is caught by download_addon's own `except Exception` and wrapped in a DownloadError, so callers see a failed download rather than a crash.

Source

Thrown at qt/aqt/addons.py:1122

# Downloading
######################################################################


def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError:
    "Fetch a single add-on from AnkiWeb."
    try:
        resp = client.get(f"{aqt.appShared}download/{id}?v=2.1&p={_current_version}")
        if resp.status_code != 200:
            return DownloadError(status_code=resp.status_code)

        data = client.stream_content(resp)

        match = re.match(
            "attachment; filename=(.+)", resp.headers["content-disposition"]
        )
        if match is None:
            raise ValueError(
                f"Unexpected content-disposition header: {resp.headers.get('content-disposition')}"
            )
        fname = match.group(1)

        meta = extract_meta_from_download_url(resp.url)

        return DownloadOk(
            data=data,
            filename=fname,
            mod_time=meta.mod_time,
            min_point_version=meta.min_point_version,
            max_point_version=meta.max_point_version,
            branch_index=meta.branch_index,
        )
    except Exception as e:
        return DownloadError(exception=e)

View on GitHub (pinned to 2fae55543c)

Solutions

  1. Retry the download later / check network setup (proxy, VPN, hosts file) to confirm you are actually reaching AnkiWeb and receiving the real add-on response.
  2. Upgrade Anki to the latest version so any server-side header-format changes are handled by updated parsing code in addons.py.
  3. Install the add-on manually: download the .ankiaddon file in a browser and use Tools > Add-ons > Install from file.
  4. Patch/wrap download_addon to parse the header more tolerantly (accept filename*=/quoted forms) or fall back to a constructed filename like `<id>.ankiaddon`.

Example fix

// before
match = re.match(
    "attachment; filename=(.+)", resp.headers["content-disposition"]
)
if match is None:
    raise ValueError(
        f"Unexpected content-disposition header: {resp.headers.get('content-disposition')}"
    )
fname = match.group(1)

// after
disposition = resp.headers.get("content-disposition", "")
match = re.match("attachment; filename\*?=(?:UTF-8''|\"?)([^\";]+)", disposition)
if match is None:
    # tolerate missing header by deriving filename from the add-on id
    fname = f"{id}.ankiaddon"
else:
    fname = match.group(1)
Defensive patterns

Strategy: validation

Validate before calling

disposition = resp.headers.get("content-disposition", "")
if not re.match("attachment; filename=.+", disposition):
    print(f"Skipping download: bad content-disposition {disposition!r}")
    return DownloadError(exception=ValueError(disposition))

Type guard

def is_attachment_disposition(headers) -> bool:
    return bool(re.match("attachment; filename=.+", headers.get("content-disposition", "")))

Try / catch

result = download_addon(client, addon_id)
if isinstance(result, DownloadError):
    showWarning(f"Add-on download failed: {result.exception or result.status_code}")
    # note: download_addon already wraps the ValueError in DownloadError

Prevention

When it happens

Trigger: Calling download_addon (or download_and_install_addon) when the AnkiWeb response's content-disposition header is absent, empty, or not of the form `attachment; filename=...` — e.g. a proxy/CDN stripping the header, a redirect/interstitial HTML page served instead of the .ankiaddon binary, or a server change to header formatting (RFC 5987 `filename*=UTF-8''...` form, quoted filename with semicolons, or `inline` disposition).

Common situations: Corporate proxies, antivirus gateways, or captive portals intercepting the AnkiWeb download and returning their own response without the header; AnkiWeb server-side changes or CDN misconfiguration; mirror/hosts-file overrides pointing appShared at a different server; heavily encoded or quoted filenames the naive regex can't parse.

Related errors


AI-assisted analysis of ankitects/anki@2fae55543c (2026-09-12). Data as JSON: /api/errors/fbfc4f3c7c31051b. Report an issue: GitHub.