mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-11 05:38:07 +00:00
fix(web): say what actually went wrong instead of "unknown"
Every failing endpoint returned "An error occurred; see logs for
details" and nothing else. That is survivable until the logs are the
thing you cannot reach: a device whose SD card was failing answered the
restart action, /system/status and /logs with that same sentence -- the
log viewer included, because journalctl could not be executed -- while
the exception underneath said
[Errno 5] Input/output error: 'systemctl'
which names the fault outright. The only endpoint that helped was
/health, and only because it happens to pass a subprocess's stderr
through. Diagnosis came down to guessing which endpoint leaked something.
Add describe_exception(), returning "TypeName: message" on one line, and
populate the `details` field that the response schema has always had and
nothing ever filled. The type alone carries information -- a bare
PermissionError says more than any generic sentence.
Exception text is not automatically safe to echo: a requests error
quotes the URL it failed on, and plugins that authenticate by query
string put their key there. Credential values are redacted while the
parameter name is kept, since knowing which credential was involved is
part of the diagnosis. Length is capped and newlines collapsed so a
parser's context cannot flood a JSON field.
Nine handlers in api_v3 bound the exception and never used it, so the
promised log entry was never written either -- "see logs for details"
was false, not merely unhelpful. Those now log with a traceback and
carry the detail. The other 60 already logged and are unchanged; they
can adopt the helper as they are touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
This commit is contained in:
co-authored by
Claude Opus 5
parent
fc25a70d75
commit
8c1171444c
+20
-3
@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.config_manager import ConfigManager
|
||||
from src.web_interface.error_handler import describe_exception
|
||||
from src.exceptions import ConfigError
|
||||
from src.plugin_system.plugin_manager import PluginManager
|
||||
from src.plugin_system.store_manager import PluginStoreManager
|
||||
@@ -391,15 +392,30 @@ def internal_error(error):
|
||||
import logging
|
||||
logger = logging.getLogger('web_interface')
|
||||
logger.error("Internal server error", exc_info=True)
|
||||
return jsonify({
|
||||
payload = {
|
||||
'status': 'error',
|
||||
'error_code': 'INTERNAL_ERROR',
|
||||
'message': 'An internal error occurred; see logs for details',
|
||||
}), 500
|
||||
}
|
||||
# Flask hands the original exception over as `error.original_exception`
|
||||
# when propagation is off; without it there is nothing to describe.
|
||||
original = getattr(error, 'original_exception', None) or (
|
||||
error if isinstance(error, BaseException) else None)
|
||||
if original is not None:
|
||||
payload['details'] = describe_exception(original)
|
||||
return jsonify(payload), 500
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_exception(error):
|
||||
"""Handle all unhandled exceptions."""
|
||||
"""Handle all unhandled exceptions.
|
||||
|
||||
Returning only "see logs for details" is fine until the logs are exactly
|
||||
what you cannot reach. A device with failing storage answered every
|
||||
endpoint with that sentence -- including the log viewer, because journalctl
|
||||
could not be executed -- while the exception underneath said
|
||||
`[Errno 5] Input/output error`. Naming the error costs nothing here and is
|
||||
frequently the whole diagnosis, so include it alongside the log pointer.
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger('web_interface')
|
||||
logger.error("Unhandled exception", exc_info=True)
|
||||
@@ -407,6 +423,7 @@ def handle_exception(error):
|
||||
'status': 'error',
|
||||
'error_code': 'UNKNOWN_ERROR',
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(error),
|
||||
}), 500
|
||||
|
||||
# Captive portal redirect middleware
|
||||
|
||||
@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
# Import new infrastructure
|
||||
from src.web_interface.api_helpers import success_response, error_response, validate_request_json
|
||||
from src.web_interface.errors import ErrorCode
|
||||
from src.web_interface.error_handler import describe_exception
|
||||
from src.plugin_system.operation_types import OperationType
|
||||
from src.web_interface.validators import (
|
||||
validate_file_upload
|
||||
@@ -289,9 +290,11 @@ def get_schedule_config():
|
||||
|
||||
return success_response(data=schedule_config)
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_LOAD_FAILED,
|
||||
"An error occurred; see logs for details",
|
||||
details=describe_exception(e),
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -1625,9 +1628,11 @@ def get_health():
|
||||
|
||||
return jsonify({'status': 'success', 'data': health_status})
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e),
|
||||
'data': {'status': 'unhealthy'}
|
||||
}), 500
|
||||
|
||||
@@ -6563,7 +6568,10 @@ def get_fonts_catalog():
|
||||
|
||||
return jsonify({'status': 'success', 'data': {'catalog': catalog}})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({'status': 'error',
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e)}), 500
|
||||
|
||||
@api_v3.route('/fonts/tokens', methods=['GET'])
|
||||
def get_font_tokens():
|
||||
@@ -7522,9 +7530,11 @@ def get_logs():
|
||||
'message': 'Timeout while fetching logs'
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred; see logs for details'
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e)
|
||||
}), 500
|
||||
|
||||
# Multi-Display Sync Endpoints
|
||||
@@ -7589,9 +7599,11 @@ def get_wifi_status():
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred; see logs for details'
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e)
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/scan', methods=['GET'])
|
||||
@@ -7764,9 +7776,11 @@ def enable_ap_mode():
|
||||
'message': message
|
||||
}), 400
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred; see logs for details'
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e)
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/ap/disable', methods=['POST'])
|
||||
@@ -7789,9 +7803,11 @@ def disable_ap_mode():
|
||||
'message': message
|
||||
}), 400
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred; see logs for details'
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e)
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/ap/auto-enable', methods=['GET'])
|
||||
@@ -7810,9 +7826,11 @@ def get_auto_enable_ap_mode():
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred; see logs for details'
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e)
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/ap/auto-enable', methods=['POST'])
|
||||
@@ -7842,9 +7860,11 @@ def set_auto_enable_ap_mode():
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error("%s failed", request.path, exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'An error occurred; see logs for details'
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'details': describe_exception(e)
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/radio', methods=['GET'])
|
||||
|
||||
Reference in New Issue
Block a user