Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions explorer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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/<miner_id>')
def miner_detail(miner_id):
Expand Down Expand Up @@ -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):
Expand All @@ -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)
app.run(host='0.0.0.0', port=5000, debug=True)
6 changes: 3 additions & 3 deletions explorer/explorer_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions explorer/test_app_error_redaction.py
Original file line number Diff line number Diff line change
@@ -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()