Full download via Python lib always fails: HttpError 400 "missing original size" (empty-collection repro)

(Also if it wasnt obvious I used AI for a bunch of these because im not that code literate yet, please excuse any mistakes)

Environment:

  • OS: Debian 13 (headless)
  • Python: 3.13
  • Anki Version: 26.8.1, 26.9.3 (anki via PyPI)
  • Comparison: Anki Desktop 26.8.1 on macOS syncs fine using the same account.

Description:

Calling col.full_upload_or_download(..., upload=False) on a headless Python instance fails instantly (~170ms) with a Rust HTTP 400 error (missing original size).

This occurs on an empty temporary collection, ruling out local data/media corruption or proxy issues.

The reproduction script:

#!/usr/bin/env python3
#Requires: pip install anki

import os
import tempfile

from anki.collection import Collection
from anki.sync_pb2 import SyncCollectionResponse

EMAIL = "YOUR-ANKIWEB-EMAIL"
PASSWORD = "YOUR-ANKIWEB-PASSWORD"

tmp = tempfile.mkdtemp(prefix="freshanki")
path = os.path.join(tmp, "collection.anki2")
col = Collection(path)
try:
auth = col.sync_login(username=EMAIL, password=PASSWORD, endpoint=None)
out = col.sync_collection(auth, sync_media=False)
print("required:", out.required)
print("host:", getattr(out, "host_number", None))
print("endpoint:", getattr(out, "new_endpoint", None))
print("media_usn:", getattr(out, "server_media_usn", None))
assert out.required in (
SyncCollectionResponse.FULL_SYNC,
SyncCollectionResponse.FULL_DOWNLOAD,
), "account does not currently require a full download; repro needs that state"
col.close_for_full_sync()
try:
col.full_upload_or_download(
auth=auth, server_usn=out.server_media_usn, upload=False
)
print("FULL-DOWNLOAD-OK")
finally:
col.reopen(after_full_sync=True)
finally:
col.close()

Log file:

2026-09-26 09:37:17,863 full-download start server_media_usn=28329 2026-09-26 09:37:18,035 full-download FAILED 'HttpError { code: 400, context: "missing original size", source: None }' 2026-09-26 09:37:28,750 handshake required=2 host_number=21 new_endpoint='``https://sync21.ankiweb.net/``' server_media_usn=28329 2026-09-26 09:37:28,750 full-download start server_media_usn=28329 2026-09-26 09:37:28,923 full-download FAILED 'HttpError { code: 400, context: "missing original size", source: None }' 2026-09-26 09:37:44,638 handshake required=2 host_number=21 new_endpoint='``https://sync21.ankiweb.net/``' server_media_usn=28329 2026-09-26 09:37:44,638 full-download start server_media_usn=28329 2026-09-26 09:37:44,811 full-download FAILED 'HttpError { code: 400, context: "missing original size", source: None }' 2026-09-26 09:38:05,522 handshake required=2 host_number=21 new_endpoint='``https://sync21.ankiweb.net/``' server_media_usn=28329 2026-09-26 09:38:05,522 full-download start server_media_usn=28329 2026-09-26 09:38:05,696 full-download FAILED 'HttpError { code: 400, context: "missing original size", source: None }' 2026-09-26 09:50:35,987 handshake required=2 host_number=21 new_endpoint='``https://sync21.ankiweb.net/``' server_media_usn=28329 2026-09-26 09:50:35,988 full-download start server_media_usn=28329 2026-09-26 09:50:36,161 full-download FAILED 'HttpError { code: 400, context: "missing original size", source: None }' 2026-09-26 09:50:41,872 handshake required=0 host_number=21 new_endpoint='``https://sync21.ankiweb.net/``' server_media_usn=28329

Additional Context:

If files (collection.anki2 and media) are manually copied from macOS to the VM, normal incremental syncs (and adding cards directly on the VM) work fine.

Edit: I was not able to fix the issue but found a workaround. When the Rust backend call 400s, I re-issue the same full download over plain HTTPS (zstd body + anki-sync header, follow redirects), verify the bytes open as sqlite, and swap the collection file. Empty-collection repro proves it’s request framing, not data.