From 63d516586029b1f832f4713bad02ab341c5869e5 Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 20 Apr 2026 18:40:19 +0800 Subject: [PATCH] ci: harden local pdc mock transport handling --- .github/workflows/release.yml | 33 ++++++++++++ scripts/pdc-mock-server.py | 95 ++++++++++++++++++++++++++++------- 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ec34a9..6164109 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1061,6 +1061,39 @@ jobs: if-no-files-found: error retention-days: 90 + - name: Dump PDC Diagnostics + if: failure() + shell: pwsh + run: | + if (Test-Path "pdc-work/pdc-mock.out.log") { + Write-Host "===== pdc-mock stdout =====" + Get-Content "pdc-work/pdc-mock.out.log" -ErrorAction SilentlyContinue | Write-Host + } + + if (Test-Path "pdc-work/pdc-mock.err.log") { + Write-Host "===== pdc-mock stderr =====" + Get-Content "pdc-work/pdc-mock.err.log" -ErrorAction SilentlyContinue | Write-Host + } + + if (Test-Path "pdc-output/mock-pdc") { + Write-Host "===== pdc-mock captured payloads =====" + Get-ChildItem "pdc-output/mock-pdc" -Recurse -File | ForEach-Object { + Write-Host "--- $($_.FullName) ---" + Get-Content $_.FullName -ErrorAction SilentlyContinue | Write-Host + } + } + + - name: Upload PDC Diagnostics Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: pdc-diagnostics + path: | + pdc-work/pdc-mock*.log + pdc-output/mock-pdc/** + if-no-files-found: ignore + retention-days: 30 + github-release: needs: [ prepare, build-windows, build-linux, build-macos, publish-pdc ] runs-on: ubuntu-latest diff --git a/scripts/pdc-mock-server.py b/scripts/pdc-mock-server.py index 3cd11ae..86bb8f0 100644 --- a/scripts/pdc-mock-server.py +++ b/scripts/pdc-mock-server.py @@ -12,6 +12,7 @@ def _utc_now_text() -> str: class PdcMockHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" token = "" data_dir = Path(".") @@ -20,22 +21,74 @@ class PdcMockHandler(BaseHTTPRequestHandler): self.send_response(status_code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) + self.send_header("Connection", "close") self.end_headers() self.wfile.write(body) + self.wfile.flush() + self.close_connection = True + + def handle_expect_100(self) -> bool: + self.send_response_only(100) + self.end_headers() + return True + + def _read_chunked_body(self) -> bytes: + chunks = bytearray() + while True: + size_line = self.rfile.readline() + if not size_line: + break + + size_line = size_line.strip() + if not size_line: + continue + + size_text = size_line.split(b";", 1)[0] + chunk_size = int(size_text, 16) + if chunk_size == 0: + # Consume optional trailer headers until the terminating blank line. + while True: + trailer = self.rfile.readline() + if trailer in (b"", b"\r\n", b"\n"): + break + break + + remaining = chunk_size + while remaining > 0: + part = self.rfile.read(remaining) + if not part: + raise ConnectionError("unexpected end of stream while reading chunked request body") + chunks.extend(part) + remaining -= len(part) + + chunk_terminator = self.rfile.read(2) + if chunk_terminator == b"\r\n": + continue + if chunk_terminator[:1] != b"\n": + raise ValueError("invalid chunk terminator") + + return bytes(chunks) + + def _read_request_body(self) -> bytes: + transfer_encoding = (self.headers.get("Transfer-Encoding") or "").lower() + if "chunked" in transfer_encoding: + return self._read_chunked_body() - def _read_json_body(self) -> dict: length = int(self.headers.get("Content-Length", "0")) if length <= 0: - return {} - raw = self.rfile.read(length) - if not raw: - return {} - try: - return json.loads(raw.decode("utf-8")) - except Exception: - return {} + return b"" + return self.rfile.read(length) - def _save_payload(self, name: str, payload: dict) -> None: + def _read_json_body(self) -> tuple[dict, bytes]: + raw = self._read_request_body() + if not raw: + return {}, raw + try: + return json.loads(raw.decode("utf-8")), raw + except Exception: + return {}, raw + + def _save_payload(self, name: str, payload: dict, raw_body: bytes) -> None: out = self.data_dir / f"{name}.json" out.parent.mkdir(parents=True, exist_ok=True) out.write_text( @@ -43,6 +96,10 @@ class PdcMockHandler(BaseHTTPRequestHandler): { "savedAtUtc": _utc_now_text(), "path": self.path, + "method": self.command, + "headers": {key: value for key, value in self.headers.items()}, + "rawBodyLength": len(raw_body), + "rawBodyPreview": raw_body[:4096].decode("utf-8", errors="replace"), "payload": payload, }, ensure_ascii=False, @@ -66,16 +123,23 @@ class PdcMockHandler(BaseHTTPRequestHandler): self._write_json(404, {"error": "not_found", "path": self.path}) def do_POST(self) -> None: + print( + f"[pdc-mock] {self.command} {self.path} " + f"content-length={self.headers.get('Content-Length', '')} " + f"transfer-encoding={self.headers.get('Transfer-Encoding', '')} " + f"expect={self.headers.get('Expect', '')}" + ) + if not self._check_token(): self._write_json(401, {"error": "unauthorized"}) return - payload = self._read_json_body() + payload, raw_body = self._read_json_body() if self.path == "/api/v1/fileMaps/diff": items = payload.get("items") if isinstance(payload, dict) else {} keys = sorted(items.keys()) if isinstance(items, dict) else [] - self._save_payload("filemaps-diff-request", payload) + self._save_payload("filemaps-diff-request", payload, raw_body) # CI fallback mode: return empty diff to avoid long object uploads # against a local mock endpoint. Real PDC endpoint will return # actual missing object hashes. @@ -91,7 +155,7 @@ class PdcMockHandler(BaseHTTPRequestHandler): return if self.path == "/api/v1/fileMaps/upload": - self._save_payload("filemaps-upload-request", payload) + self._save_payload("filemaps-upload-request", payload, raw_body) result = { "success": True, "code": 0, @@ -106,14 +170,11 @@ class PdcMockHandler(BaseHTTPRequestHandler): if m: primary_version = m.group(1) version = m.group(2) - distribution_id = f"{primary_version}-{version}" - self._save_payload("distribution-request", payload) + self._save_payload("distribution-request", payload, raw_body) result = { "success": True, "code": 0, "message": "ok", - "content": {"distributionId": distribution_id}, - "Content": {"distributionId": distribution_id}, } self._write_json(200, result) return