From fc401c912b73d71fc6c6518ae46af7e3742eb487 Mon Sep 17 00:00:00 2001 From: mountain Date: Thu, 9 Jul 2026 20:18:44 +0800 Subject: [PATCH] fix: tolerate empty body on legacy delete_document delete_document unconditionally called response.json(), so a successful DELETE that returns 200 with an empty body (the documented examples don't consume one, and REST APIs commonly return no content for deletes) would raise JSONDecodeError even though the document was already deleted. Return {} when the response has no content, else parse the JSON body as before. --- pageindex/cloud_api.py | 6 +++++- tests/test_legacy_sdk_contract.py | 28 +++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pageindex/cloud_api.py b/pageindex/cloud_api.py index b8f499a..1e583d7 100644 --- a/pageindex/cloud_api.py +++ b/pageindex/cloud_api.py @@ -230,7 +230,11 @@ class LegacyCloudAPI: f"/doc/{self._enc(doc_id)}/", "Failed to delete document", ) - return response.json() + # A successful DELETE may come back with an empty body (the documented + # examples don't consume one, and REST APIs commonly return no content + # for deletes). Don't let json() raise JSONDecodeError on success — + # the document is already gone; return an empty dict. + return response.json() if response.content else {} def list_documents( self, diff --git a/tests/test_legacy_sdk_contract.py b/tests/test_legacy_sdk_contract.py index 44a9938..27cff08 100644 --- a/tests/test_legacy_sdk_contract.py +++ b/tests/test_legacy_sdk_contract.py @@ -1,3 +1,5 @@ +import json + import pytest import requests @@ -7,14 +9,18 @@ from pageindex.client import CloudClient class FakeResponse: - def __init__(self, status_code=200, payload=None, text="ok", lines=None): + def __init__(self, status_code=200, payload=None, text="ok", lines=None, content=b"{}"): self.status_code = status_code self._payload = payload or {} self.text = text self._lines = lines or [] self.closed = False + # Raw body bytes; empty bytes model a no-content success (e.g. DELETE). + self.content = content def json(self): + if not self.content: + raise json.JSONDecodeError("Expecting value", "", 0) return self._payload def iter_lines(self): @@ -259,6 +265,26 @@ def test_chat_completions_stream_errors_are_pageindex_api_error(monkeypatch): list(stream) +def test_delete_document_tolerates_empty_success_body(monkeypatch): + # A successful DELETE may return 200 with no body; delete_document must not + # raise JSONDecodeError parsing an empty response (the doc is already gone). + def fake_request(method, url, **kwargs): + assert method == "DELETE" + return FakeResponse(status_code=200, content=b"") + + monkeypatch.setattr("pageindex.cloud_api.requests.request", fake_request) + assert PageIndexClient("pi-test").delete_document("doc-1") == {} + + +def test_delete_document_returns_json_body_when_present(monkeypatch): + # When the server does return a body, it's parsed and passed through. + def fake_request(method, url, **kwargs): + return FakeResponse(status_code=200, payload={"deleted": True}, content=b'{"deleted": true}') + + monkeypatch.setattr("pageindex.cloud_api.requests.request", fake_request) + assert PageIndexClient("pi-test").delete_document("doc-1") == {"deleted": True} + + def test_api_errors_are_pageindex_api_error(monkeypatch): def fake_request(*args, **kwargs): return FakeResponse(status_code=500, text="server error")