From 4680a7a30b8e57501b5e0cfe7864da2528e1cc89 Mon Sep 17 00:00:00 2001 From: Avi Seth Date: Mon, 10 Aug 2026 13:14:00 +0000 Subject: [PATCH] Include pushgateway response body in HTTP error messages When the pushgateway rejects a push it explains why in the response body, but urllib's HTTPError only renders "HTTP Error 500: Internal Server Error", so the diagnostic was discarded before the caller ever saw it. Read the body in _make_handler and append it to the error message, still raising an HTTPError so existing except clauses keep working, and include the body in the OSError raised for handlers that suppress urllib's default error handling. Fixes #96 Signed-off-by: Avi Seth --- prometheus_client/exposition.py | 13 +++++++++++-- tests/test_exposition.py | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 0b63f6f6..8cdc05e6 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -515,9 +515,18 @@ def handle() -> None: request.get_method = lambda: method # type: ignore for k, v in headers: request.add_header(k, v) - resp = build_opener(base_handler).open(request, timeout=timeout) + try: + resp = build_opener(base_handler).open(request, timeout=timeout) + except HTTPError as e: + # The pushgateway reports what was wrong with the request in the + # response body, so include it in the error message. + body = e.read().decode('utf-8', 'replace').strip() + if not body: + raise + raise HTTPError(e.url, e.code, f'{e.reason} ({body})', e.headers, None) from e if resp.code >= 400: - raise OSError(f"error talking to pushgateway: {resp.code} {resp.msg}") + body = resp.read().decode('utf-8', 'replace').strip() + raise OSError(f"error talking to pushgateway: {resp.code} {resp.msg} ({body})") return handle diff --git a/tests/test_exposition.py b/tests/test_exposition.py index 1885480f..f19940bd 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -231,7 +231,12 @@ def setUp(self): class TestHandler(BaseHTTPRequestHandler): def do_PUT(self): - if 'with_basic_auth' in self.requestline and self.headers['authorization'] != 'Basic Zm9vOmJhcg==': + error_body = None + if 'error' in self.requestline: + error_body = b'text format parsing error in line 1' + self.send_response(500) + self.send_header('Content-Length', str(len(error_body))) + elif 'with_basic_auth' in self.requestline and self.headers['authorization'] != 'Basic Zm9vOmJhcg==': self.send_response(401) elif 'redirect' in self.requestline and redirect_flag not in self.requestline: # checks for an initial test request with 'redirect' but without the redirect_flag, @@ -243,6 +248,8 @@ def do_PUT(self): length = int(self.headers['content-length']) requests.append((self, self.rfile.read(length))) self.end_headers() + if error_body: + self.wfile.write(error_body) do_POST = do_PUT do_DELETE = do_PUT @@ -324,6 +331,12 @@ def test_push_with_complex_job(self): self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_PLAIN_0_0_4) self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n') + def test_push_error_includes_response_body(self): + with self.assertRaises(urllib.error.HTTPError) as cm: + push_to_gateway(self.address, "error_job", self.registry) + self.assertEqual(cm.exception.code, 500) + self.assertIn('text format parsing error in line 1', str(cm.exception)) + def test_pushadd(self): pushadd_to_gateway(self.address, "my_job", self.registry) self.assertEqual(self.requests[0][0].command, 'POST')