From ffd01ebcf3faa649a97a104e03bb3e56ddca0b68 Mon Sep 17 00:00:00 2001 From: JesseMarkowitz Date: Mon, 17 Aug 2026 10:14:18 -0400 Subject: [PATCH] tools: print the error body the Library endpoint returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pairing came back perfectly clean, and it is the whole diagnosis: has library_file_id → refused (11/11) no library_file_id → deleted (7/7) And /files/{libfile_id}/download answered 200 — not 403, not 404 — with {status, error_code, error_type, error_message}. That endpoint accepts the Library id; it is returning an application-level error inside an HTTP 200. The probe printed only the keys and dropped the message, which is the one thing that says what the call is missing. - show() now prints the full body for every response, and detects a non-JSON 200 as possible raw bytes. - Added routes worth ruling in or out: the same call as POST, with a conversation_id, /files/{lib}/content, /content?asset_pointer=, and four Library listing endpoints — a listing usually reveals both the id form the UI uses and the route that actually serves bytes. - Probes a second Library id too, so one odd file cannot mislead us. --- tools/library_download_probe.py | 75 ++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/tools/library_download_probe.py b/tools/library_download_probe.py index 19f002a..2777bdf 100644 --- a/tools/library_download_probe.py +++ b/tools/library_download_probe.py @@ -80,7 +80,7 @@ def main() -> None: return provider = ChatGPTProvider() - pairs: list[tuple[str, str]] = [] + pairs: list[tuple[str, str, str]] = [] print("=" * 78) print("Pairing refused assets with their Library IDs") @@ -98,61 +98,78 @@ def main() -> None: source = (att or {}).get("source") print(f" {file_id[:34]}… library={str(lib)[:34]:<36} source={source}") if lib: - pairs.append((file_id, lib)) + pairs.append((file_id, lib, conv_id)) if not pairs: print("\n No refused asset carries a library_file_id — different cause.") return - file_id, lib_id = pairs[0] + file_id, lib_id, conv_id = pairs[0] print() print("=" * 78) print(f"Endpoint hunt for {lib_id}") print(f" (sediment id {file_id})") print("=" * 78) - def show(label: str, url: str) -> bool: + def show(label: str, url: str, method: str = "GET") -> bool: + """Print status AND body. A 200 carrying an error envelope says what + the endpoint wants — printing only the keys threw that away.""" try: provider._pace() - resp = provider._session.request("GET", url, timeout=30) + resp = provider._session.request(method, url, timeout=30) except Exception as e: # noqa: BLE001 - diagnostic print(f" {label:<50} error {type(e).__name__}") return False - note = "" + hit = False - if resp.status_code == 200: - try: - body = resp.json() - if isinstance(body, dict) and body.get("download_url"): - note = " ← DOWNLOAD URL" - hit = True - else: - keys = list(body)[:8] if isinstance(body, dict) else type(body).__name__ - note = f" 200 keys={keys}" - except Exception: # noqa: BLE001 - diagnostic - note = f" 200 non-JSON ({len(resp.content)} bytes)" - hit = True - print(f" {label:<50} {resp.status_code}{note}") + try: + body = resp.json() + except Exception: # noqa: BLE001 - diagnostic + preview = (resp.text or "").strip()[:200] + if resp.status_code == 200 and resp.content: + print(f" {label:<50} 200 non-JSON ({len(resp.content)} bytes) ← BYTES?") + return True + print(f" {label:<50} {resp.status_code} {preview}") + return False + + if isinstance(body, dict) and body.get("download_url"): + print(f" {label:<50} {resp.status_code} ← DOWNLOAD URL") + print(f" {json.dumps(body, default=str)[:300]}") + return True + + print(f" {label:<50} {resp.status_code}") + print(f" {json.dumps(body, default=str)[:400]}") return hit candidates = [ + # This one already returns 200 with an error envelope — read it. ("/files/{lib}/download", f"{BASE_URL}/files/{lib_id}/download"), - ("/files/{lib}", f"{BASE_URL}/files/{lib_id}"), - ("/library/files/{lib}/download", f"{BASE_URL}/library/files/{lib_id}/download"), - ("/library/files/{lib}", f"{BASE_URL}/library/files/{lib_id}"), - ("/library/{lib}", f"{BASE_URL}/library/{lib_id}"), - ( - "/files/{sediment}/download?library_file_id=", - f"{BASE_URL}/files/{file_id}/download?library_file_id={lib_id}", - ), - ("/files/{lib}/download?use_case=gizmo", f"{BASE_URL}/files/{lib_id}/download?use_case=gizmo"), + ("/files/{lib}/download?conversation_id=", f"{BASE_URL}/files/{lib_id}/download?conversation_id={conv_id}"), + ("/files/{lib}/download (POST)", f"{BASE_URL}/files/{lib_id}/download"), + # How does the UI enumerate the Library? A listing tends to reveal both + # the id form it uses and the route that serves bytes. + ("/library/files?limit=3", f"{BASE_URL}/library/files?limit=3"), + ("/library?limit=3", f"{BASE_URL}/library?limit=3"), + ("/files?limit=3", f"{BASE_URL}/files?limit=3"), + ("/my_files?limit=3", f"{BASE_URL}/my_files?limit=3"), + # Content routes that serve the asset rather than a signed URL. + ("/files/{lib}/content", f"{BASE_URL}/files/{lib_id}/content"), + ("/content?asset_pointer=sediment://{sediment}", f"{BASE_URL}/content?asset_pointer=sediment://{file_id}"), ] winners = [] for label, url in candidates: - if show(label, url): + method = "POST" if "(POST)" in label else "GET" + if show(label, url, method): winners.append((label, url)) + # If a second file behaves differently, the first one was the anomaly. + if len(pairs) > 1: + _other_file, other_lib, _other_conv = pairs[1] + print() + print(f" --- second sample: {other_lib} ---") + show("/files/{lib}/download", f"{BASE_URL}/files/{other_lib}/download") + print() print("=" * 78) print("Result")