Pass upstream policy errors through PyPI proxy to users

- Add _parse_upstream_error() to extract policy messages from JFrog/Artifactory
- Pass through 403 and other 4xx errors with detailed messages
- Pin babel and electron-to-chromium to older versions for CI compatibility
This commit is contained in:
Mondo Diaz
2026-02-03 08:09:08 -06:00
parent eb11efd001
commit b2a8c7cfcc
3 changed files with 147 additions and 77 deletions

View File

@@ -6,6 +6,7 @@ Artifacts are cached on first access through configured upstream sources.
""" """
import hashlib import hashlib
import json
import logging import logging
import os import os
import re import re
@@ -33,6 +34,55 @@ PROXY_CONNECT_TIMEOUT = 30.0
PROXY_READ_TIMEOUT = 60.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]: def _extract_pypi_version(filename: str) -> Optional[str]:
"""Extract version from PyPI filename. """Extract version from PyPI filename.
@@ -207,6 +257,7 @@ async def pypi_simple_index(
# Try each source in priority order # Try each source in priority order
last_error = None last_error = None
last_status = None
for source in sources: for source in sources:
try: try:
headers = {"User-Agent": "Orchard-PyPI-Proxy/1.0"} headers = {"User-Agent": "Orchard-PyPI-Proxy/1.0"}
@@ -252,21 +303,29 @@ async def pypi_simple_index(
return HTMLResponse(content=content) 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: except httpx.ConnectError as e:
last_error = f"Connection failed: {e}" last_error = f"Connection failed: {e}"
last_status = 502
logger.warning(f"PyPI proxy: failed to connect to {source.url}: {e}") logger.warning(f"PyPI proxy: failed to connect to {source.url}: {e}")
except httpx.TimeoutException as e: except httpx.TimeoutException as e:
last_error = f"Timeout: {e}" last_error = f"Timeout: {e}"
last_status = 504
logger.warning(f"PyPI proxy: timeout connecting to {source.url}: {e}") logger.warning(f"PyPI proxy: timeout connecting to {source.url}: {e}")
except Exception as e: except Exception as e:
last_error = str(e) last_error = str(e)
last_status = 502
logger.warning(f"PyPI proxy: error fetching from {source.url}: {e}") 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( raise HTTPException(
status_code=502, status_code=status_code,
detail=f"Failed to fetch package index from upstream: {last_error}" detail=f"Upstream error: {last_error}"
) )
@@ -296,6 +355,7 @@ async def pypi_package_versions(
# Try each source in priority order # Try each source in priority order
last_error = None last_error = None
last_status = None
for source in sources: for source in sources:
try: try:
headers = {"User-Agent": "Orchard-PyPI-Proxy/1.0"} headers = {"User-Agent": "Orchard-PyPI-Proxy/1.0"}
@@ -348,23 +408,32 @@ async def pypi_package_versions(
if response.status_code == 404: if response.status_code == 404:
# Package not found in this source, try next # Package not found in this source, try next
last_error = f"Package not found in {source.name}" last_error = f"Package not found in {source.name}"
last_status = 404
continue 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: except httpx.ConnectError as e:
last_error = f"Connection failed: {e}" last_error = f"Connection failed: {e}"
last_status = 502
logger.warning(f"PyPI proxy: failed to connect to {source.url}: {e}") logger.warning(f"PyPI proxy: failed to connect to {source.url}: {e}")
except httpx.TimeoutException as e: except httpx.TimeoutException as e:
last_error = f"Timeout: {e}" last_error = f"Timeout: {e}"
last_status = 504
logger.warning(f"PyPI proxy: timeout connecting to {source.url}: {e}") logger.warning(f"PyPI proxy: timeout connecting to {source.url}: {e}")
except Exception as e: except Exception as e:
last_error = str(e) last_error = str(e)
last_status = 502
logger.warning(f"PyPI proxy: error fetching {package_name} from {source.url}: {e}") 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( raise HTTPException(
status_code=404, status_code=status_code,
detail=f"Package '{package_name}' not found: {last_error}" detail=f"Package '{package_name}' error: {last_error}"
) )
@@ -484,9 +553,12 @@ async def pypi_download_file(
redirect_count += 1 redirect_count += 1
if response.status_code != 200: 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( raise HTTPException(
status_code=response.status_code, 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') content_type = response.headers.get('content-type', 'application/octet-stream')

View File

@@ -68,44 +68,44 @@
"dev": true "dev": true
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.28.5", "@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0", "js-tokens": "^4.0.0",
"picocolors": "^1.1.1" "picocolors": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.29.0", "version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.29.0", "version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@ampproject/remapping": "^2.2.0",
"@babel/generator": "^7.29.0", "@babel/code-frame": "^7.26.0",
"@babel/helper-compilation-targets": "^7.28.6", "@babel/generator": "^7.26.0",
"@babel/helper-module-transforms": "^7.28.6", "@babel/helper-compilation-targets": "^7.25.9",
"@babel/helpers": "^7.28.6", "@babel/helper-module-transforms": "^7.26.0",
"@babel/parser": "^7.29.0", "@babel/helpers": "^7.26.0",
"@babel/template": "^7.28.6", "@babel/parser": "^7.26.0",
"@babel/traverse": "^7.29.0", "@babel/template": "^7.25.9",
"@babel/types": "^7.29.0", "@babel/traverse": "^7.25.9",
"@jridgewell/remapping": "^2.3.5", "@babel/types": "^7.26.0",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@@ -121,15 +121,15 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.29.0", "version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.0", "@babel/parser": "^7.26.3",
"@babel/types": "^7.29.0", "@babel/types": "^7.26.3",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
}, },
"engines": { "engines": {
@@ -152,15 +152,6 @@
"node": ">=6.9.0" "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": { "node_modules/@babel/helper-module-imports": {
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
@@ -241,12 +232,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.29.0", "version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.29.0" "@babel/types": "^7.26.3"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -309,31 +300,31 @@
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.29.0", "version": "7.26.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.26.3",
"@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.26.3",
"@babel/parser": "^7.29.0", "@babel/template": "^7.25.9",
"@babel/template": "^7.28.6", "@babel/types": "^7.26.3",
"@babel/types": "^7.29.0", "debug": "^4.3.1",
"debug": "^4.3.1" "globals": "^11.1.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.29.0", "version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.28.5" "@babel/helper-validator-identifier": "^7.25.9"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -854,16 +845,6 @@
"@jridgewell/trace-mapping": "^0.3.24" "@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": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -2530,9 +2511,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.286", "version": "1.5.72",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz",
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw==",
"dev": true "dev": true
}, },
"node_modules/entities": { "node_modules/entities": {
@@ -2848,6 +2829,15 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",

View File

@@ -38,6 +38,14 @@
"rollup": "4.52.4", "rollup": "4.52.4",
"caniuse-lite": "1.0.30001692", "caniuse-lite": "1.0.30001692",
"baseline-browser-mapping": "2.9.5", "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"
} }
} }