diff --git a/backend/app/pypi_proxy.py b/backend/app/pypi_proxy.py index 89bc072..46011be 100644 --- a/backend/app/pypi_proxy.py +++ b/backend/app/pypi_proxy.py @@ -6,6 +6,7 @@ Artifacts are cached on first access through configured upstream sources. """ import hashlib +import json import logging import os import re @@ -33,6 +34,55 @@ PROXY_CONNECT_TIMEOUT = 30.0 PROXY_READ_TIMEOUT = 60.0 +def _parse_upstream_error(response: httpx.Response) -> str: + """Parse upstream error response to extract useful error details. + + Handles JFrog/Artifactory policy errors and other common formats. + Returns a user-friendly error message. + """ + status = response.status_code + + try: + body = response.text + except Exception: + return f"HTTP {status}" + + # Try to parse as JSON (JFrog/Artifactory format) + try: + data = json.loads(body) + + # JFrog Artifactory format: {"errors": [{"status": 403, "message": "..."}]} + if "errors" in data and isinstance(data["errors"], list): + messages = [] + for err in data["errors"]: + if isinstance(err, dict) and "message" in err: + messages.append(err["message"]) + if messages: + return "; ".join(messages) + + # Alternative format: {"message": "..."} + if "message" in data: + return data["message"] + + # Alternative format: {"error": "..."} + if "error" in data: + return data["error"] + + except (json.JSONDecodeError, ValueError): + pass + + # Check for policy-related keywords in plain text response + policy_keywords = ["policy", "blocked", "forbidden", "curation", "security"] + if any(kw in body.lower() for kw in policy_keywords): + # Truncate long responses but preserve the message + if len(body) > 500: + return body[:500] + "..." + return body + + # Default: just return status code + return f"HTTP {status}" + + def _extract_pypi_version(filename: str) -> Optional[str]: """Extract version from PyPI filename. @@ -207,6 +257,7 @@ async def pypi_simple_index( # Try each source in priority order last_error = None + last_status = None for source in sources: try: headers = {"User-Agent": "Orchard-PyPI-Proxy/1.0"} @@ -252,21 +303,29 @@ async def pypi_simple_index( return HTMLResponse(content=content) - last_error = f"HTTP {response.status_code}" + # Parse upstream error for policy/blocking messages + last_error = _parse_upstream_error(response) + last_status = response.status_code + logger.warning(f"PyPI proxy: upstream returned {response.status_code}: {last_error}") except httpx.ConnectError as e: last_error = f"Connection failed: {e}" + last_status = 502 logger.warning(f"PyPI proxy: failed to connect to {source.url}: {e}") except httpx.TimeoutException as e: last_error = f"Timeout: {e}" + last_status = 504 logger.warning(f"PyPI proxy: timeout connecting to {source.url}: {e}") except Exception as e: last_error = str(e) + last_status = 502 logger.warning(f"PyPI proxy: error fetching from {source.url}: {e}") + # Pass through 4xx errors (like 403 policy blocks) so users understand why + status_code = last_status if last_status and 400 <= last_status < 500 else 502 raise HTTPException( - status_code=502, - detail=f"Failed to fetch package index from upstream: {last_error}" + status_code=status_code, + detail=f"Upstream error: {last_error}" ) @@ -296,6 +355,7 @@ async def pypi_package_versions( # Try each source in priority order last_error = None + last_status = None for source in sources: try: headers = {"User-Agent": "Orchard-PyPI-Proxy/1.0"} @@ -348,23 +408,32 @@ async def pypi_package_versions( if response.status_code == 404: # Package not found in this source, try next last_error = f"Package not found in {source.name}" + last_status = 404 continue - last_error = f"HTTP {response.status_code}" + # Parse upstream error for policy/blocking messages + last_error = _parse_upstream_error(response) + last_status = response.status_code + logger.warning(f"PyPI proxy: upstream returned {response.status_code} for {package_name}: {last_error}") except httpx.ConnectError as e: last_error = f"Connection failed: {e}" + last_status = 502 logger.warning(f"PyPI proxy: failed to connect to {source.url}: {e}") except httpx.TimeoutException as e: last_error = f"Timeout: {e}" + last_status = 504 logger.warning(f"PyPI proxy: timeout connecting to {source.url}: {e}") except Exception as e: last_error = str(e) + last_status = 502 logger.warning(f"PyPI proxy: error fetching {package_name} from {source.url}: {e}") + # Pass through 4xx errors (like 403 policy blocks) so users understand why + status_code = last_status if last_status and 400 <= last_status < 500 else 404 raise HTTPException( - status_code=404, - detail=f"Package '{package_name}' not found: {last_error}" + status_code=status_code, + detail=f"Package '{package_name}' error: {last_error}" ) @@ -484,9 +553,12 @@ async def pypi_download_file( redirect_count += 1 if response.status_code != 200: + # Parse upstream error for policy/blocking messages + error_detail = _parse_upstream_error(response) + logger.warning(f"PyPI proxy: upstream returned {response.status_code} for {filename}: {error_detail}") raise HTTPException( status_code=response.status_code, - detail=f"Upstream returned {response.status_code}" + detail=f"Upstream error: {error_detail}" ) content_type = response.headers.get('content-type', 'application/octet-stream') diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8d4ee83..7eba412 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -68,44 +68,44 @@ "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -121,15 +121,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" }, "engines": { @@ -152,15 +152,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -241,12 +232,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "dev": true, "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.26.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -309,31 +300,31 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -854,16 +845,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2530,9 +2511,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.72", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz", + "integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw==", "dev": true }, "node_modules/entities": { @@ -2848,6 +2829,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 12c91a3..a49bd7f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,14 @@ "rollup": "4.52.4", "caniuse-lite": "1.0.30001692", "baseline-browser-mapping": "2.9.5", - "lodash": "4.17.21" + "lodash": "4.17.21", + "electron-to-chromium": "1.5.72", + "@babel/core": "7.26.0", + "@babel/traverse": "7.26.4", + "@babel/types": "7.26.3", + "@babel/compat-data": "7.26.3", + "@babel/parser": "7.26.3", + "@babel/generator": "7.26.3", + "@babel/code-frame": "7.26.2" } }