Files
franzz ec75ac15b0
Deploy Solar Relay / deploy (push) Failing after 4s
Initial commit
2026-11-01 22:33:32 +01:00

64 lines
2.5 KiB
Python

"""Bounded JSON requests; redirects cannot forward upload credentials."""
import json
from http.client import HTTPException
from urllib.error import HTTPError, URLError
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from . import __version__
class TransportError(Exception):
pass
class NoRedirects(HTTPRedirectHandler):
def redirect_request(self, request, response, code, message, headers, new_url):
return None
def _invalid_constant(value):
raise ValueError("Non-finite JSON number")
class JsonHttp:
MAX_RESPONSE_BYTES = 2 * 1024 * 1024
def __init__(self, timeout: float, *, local: bool = False):
handlers = [NoRedirects()]
if local:
# LAN traffic must not go through a workstation's HTTP(S) proxy.
handlers.append(ProxyHandler({}))
self.opener = build_opener(*handlers)
self.timeout = timeout
def request(self, url: str, *, payload: dict | None = None, token: str | None = None, extra_headers=None) -> dict:
headers = {"Accept": "application/json", "User-Agent": f"solar-relay/{__version__}"}
headers.update(extra_headers or {})
body = None
if payload is not None:
body = json.dumps(payload, allow_nan=False, separators=(",", ":")).encode("utf-8")
headers["Content-Type"] = "application/json"
if token:
headers["Authorization"] = f"Bearer {token}"
request = Request(url, data=body, headers=headers, method="POST" if body is not None else "GET")
try:
with self.opener.open(request, timeout=self.timeout) as response:
raw = response.read(self.MAX_RESPONSE_BYTES + 1)
except HTTPError as error:
# Do not log server bodies: they can echo request headers or credentials.
error.close()
raise TransportError(f"HTTP {error.code}") from None
except (URLError, OSError, TimeoutError, HTTPException) as error:
raise TransportError(f"Network request failed ({type(error).__name__})") from None
if len(raw) > self.MAX_RESPONSE_BYTES:
raise TransportError("JSON response exceeds 2 MiB")
try:
value = json.loads(raw, parse_constant=_invalid_constant)
except (ValueError, UnicodeError, RecursionError):
raise TransportError("Response is not valid JSON") from None
if not isinstance(value, dict):
raise TransportError("Expected a JSON object")
return value