From 9e59ea0a579146c3d282bd875adb1390ab1b85f9 Mon Sep 17 00:00:00 2001 From: dazer1234 Date: Mon, 18 May 2026 16:04:22 +0200 Subject: [PATCH] Redact explorer upstream errors --- explorer/app.py | 25 +++++++++++++----- explorer/explorer_server.py | 6 ++--- explorer/test_app_error_redaction.py | 38 ++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 explorer/test_app_error_redaction.py diff --git a/explorer/app.py b/explorer/app.py index 872a1019c..ac75de8aa 100644 --- a/explorer/app.py +++ b/explorer/app.py @@ -9,6 +9,14 @@ API_BASE_URL = "http://localhost:8000" MINERS_ENDPOINT = f"{API_BASE_URL}/api/miners" + +def _connection_error_response(payload=None): + body = {'error': 'Connection error'} + if payload: + body.update(payload) + return jsonify(body), 502 + + @app.route('/') def dashboard(): return render_template('dashboard.html') @@ -49,8 +57,9 @@ def get_miners(): return jsonify(miners_data) else: return jsonify({'error': 'Failed to fetch miners data', 'miners': []}), 500 - except requests.exceptions.RequestException as e: - return jsonify({'error': f'Connection error: {str(e)}', 'miners': []}), 500 + except requests.exceptions.RequestException: + app.logger.warning("Failed to fetch miners data from upstream", exc_info=True) + return _connection_error_response({'miners': []}) @app.route('/api/network/stats') def get_network_stats(): @@ -80,8 +89,9 @@ def get_network_stats(): return jsonify(stats) else: return jsonify({'error': 'Failed to fetch network stats'}), 500 - except requests.exceptions.RequestException as e: - return jsonify({'error': f'Connection error: {str(e)}'}), 500 + except requests.exceptions.RequestException: + app.logger.warning("Failed to fetch network stats from upstream", exc_info=True) + return _connection_error_response() @app.route('/miner/') def miner_detail(miner_id): @@ -122,8 +132,9 @@ def get_miner_detail(miner_id): return jsonify({'error': 'Miner not found'}), 404 else: return jsonify({'error': 'Failed to fetch miner data'}), 500 - except requests.exceptions.RequestException as e: - return jsonify({'error': f'Connection error: {str(e)}'}), 500 + except requests.exceptions.RequestException: + app.logger.warning("Failed to fetch miner detail from upstream", exc_info=True) + return _connection_error_response() @app.errorhandler(404) def not_found(error): @@ -134,4 +145,4 @@ def internal_error(error): return render_template('500.html'), 500 if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/explorer/explorer_server.py b/explorer/explorer_server.py index 16f3f8ce9..8f0e1f2c8 100644 --- a/explorer/explorer_server.py +++ b/explorer/explorer_server.py @@ -92,8 +92,8 @@ def handle_proxy(self, endpoint, parsed): self.send_json(data, headers={'X-Cache': 'MISS'}) except requests.exceptions.Timeout: self.send_error_json(504, 'Gateway Timeout') - except requests.exceptions.RequestException as e: - self.send_error_json(502, f'Bad Gateway: {str(e)}') + except requests.exceptions.RequestException: + self.send_error_json(502, 'Bad Gateway') except json.JSONDecodeError: self.send_error_json(502, 'Invalid JSON from upstream') @@ -152,7 +152,7 @@ def get_analytics_data(): response.raise_for_status() results[endpoint] = response.json() except Exception as e: - results[endpoint] = {'error': str(e)} + results[endpoint] = {'error': 'Bad Gateway'} return results diff --git a/explorer/test_app_error_redaction.py b/explorer/test_app_error_redaction.py new file mode 100644 index 000000000..a5e67a90c --- /dev/null +++ b/explorer/test_app_error_redaction.py @@ -0,0 +1,38 @@ +import unittest +from unittest.mock import patch + +import requests + +import app as explorer_app + + +class ExplorerAppErrorRedactionTests(unittest.TestCase): + def setUp(self): + explorer_app.app.config["TESTING"] = True + self.client = explorer_app.app.test_client() + + @patch("app.requests.get") + def test_miners_connection_error_does_not_leak_exception(self, mock_get): + mock_get.side_effect = requests.exceptions.ConnectionError( + "connect failed to internal-host.local:8000" + ) + + response = self.client.get("/api/miners") + + self.assertEqual(response.status_code, 502) + self.assertEqual(response.get_json(), {"error": "Connection error", "miners": []}) + self.assertNotIn("internal-host", response.get_data(as_text=True)) + + @patch("app.requests.get") + def test_miner_detail_connection_error_does_not_leak_exception(self, mock_get): + mock_get.side_effect = requests.exceptions.Timeout("secret timeout detail") + + response = self.client.get("/api/miner/alice") + + self.assertEqual(response.status_code, 502) + self.assertEqual(response.get_json(), {"error": "Connection error"}) + self.assertNotIn("secret timeout detail", response.get_data(as_text=True)) + + +if __name__ == "__main__": + unittest.main()