From fb2e28c28af30110e37fbda055b7d15e5dd81741 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Fri, 24 Apr 2026 22:28:44 -0400 Subject: [PATCH] Log partial payload + headers when a download is interrupted When a streaming download fails mid-flight (ChunkedEncodingError / ConnectionError) or yields a corrupt zip, we now log the bytes written so far, the response headers, and a UTF-8/repr preview of the first 512 bytes of what arrived. This makes it possible to tell apart genuine truncation from a tiny error-stub response that the server returned with status 200. Motivated by intermittent IncompleteRead failures fetching GitHub Actions logs where the advertised Content-Length was suspiciously small (~4 KiB), but the discarded body could have revealed whether it was a JSON/HTML stub or real binary data. Co-Authored-By: Claude Code 2.1.119 / Claude Opus 4.7 (1M context) --- src/tinuous/base.py | 57 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/tinuous/base.py b/src/tinuous/base.py index da3321e..eee4b11 100644 --- a/src/tinuous/base.py +++ b/src/tinuous/base.py @@ -43,6 +43,32 @@ platform.python_version(), ) +#: Number of bytes from the start of a partial/invalid download to include in +#: diagnostic log output. +PARTIAL_PREVIEW_BYTES = 512 + + +def format_partial_preview(data: bytes) -> str: + """Render a short, human-readable preview of partial download bytes. + + Tries UTF-8 first so JSON/HTML stub responses are legible; falls back to + ``repr(bytes)`` (with ``\\xNN`` escapes) for binary payloads. + """ + if not data: + return "" + try: + return repr(data.decode("utf-8")) + except UnicodeDecodeError: + return repr(data) + + +def _read_preview(filepath: Path) -> bytes: + try: + with filepath.open("rb") as fp: + return fp.read(PARTIAL_PREVIEW_BYTES) + except OSError: + return b"" + class CommonStatus(Enum): SUCCESS = "success" @@ -165,17 +191,29 @@ def download( i = 0 while True: try: + r: requests.Response | None = None + bytes_written = 0 try: r = self.get(path, stream=True, headers=headers) with filepath.open("wb") as fp: for chunk in r.iter_content(chunk_size=8192): fp.write(chunk) + bytes_written += len(chunk) except (ChunkedEncodingError, ReqConError) as e: if i < self.MAX_RETRIES: + resp_headers = dict(r.headers) if r is not None else None + preview = format_partial_preview(_read_preview(filepath)) log.warning( - "Download of %s interrupted: %s; waiting & retrying", + "Download of %s interrupted after %d bytes: %s;" + " response headers: %s;" + " partial preview (first %d bytes): %s;" + " waiting & retrying", path, + bytes_written, str(e), + resp_headers, + PARTIAL_PREVIEW_BYTES, + preview, ) i += 1 sleep(i) @@ -200,11 +238,26 @@ def download_zipfile(self, path: str, target_dir: Path) -> None: zf.extractall(target_dir) except BadZipFile: rmtree(target_dir) + size = zippath.stat().st_size if zippath.exists() else 0 + preview = format_partial_preview(_read_preview(zippath)) if i < self.ZIPFILE_RETRIES: - log.error("Invalid zip file retrieved; waiting and retrying") + log.error( + "Invalid zip file retrieved (%d bytes); " + "preview (first %d bytes): %s; waiting and retrying", + size, + PARTIAL_PREVIEW_BYTES, + preview, + ) i += 1 sleep(i * i) else: + log.error( + "Invalid zip file retrieved (%d bytes); " + "preview (first %d bytes): %s", + size, + PARTIAL_PREVIEW_BYTES, + preview, + ) raise except BaseException: rmtree(target_dir)