-
Notifications
You must be signed in to change notification settings - Fork 39
admin: render HTTP errors inside administration layout #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| # SPDX-FileCopyrightText: 2026 CERN. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| """Administration HTTP error page handlers.""" | ||
|
|
||
| from flask import current_app, render_template, request, url_for | ||
| from invenio_i18n import lazy_gettext as _ | ||
| from invenio_theme import views as theme_views | ||
| from werkzeug.exceptions import HTTPException | ||
|
|
||
| _ERROR_CONTENT = { | ||
| 401: { | ||
| "title": _("Sign in required"), | ||
| "message": _( | ||
| "You need to sign in before you can continue in the administration panel." | ||
| ), | ||
| }, | ||
| 403: { | ||
| "title": _("Access denied"), | ||
| "message": _("You do not have permission to access this administration page."), | ||
| }, | ||
| 404: { | ||
| "title": _("Page not found"), | ||
| "message": _("The administration page you requested could not be found."), | ||
| }, | ||
| 429: { | ||
| "title": _("Too many requests"), | ||
| "message": _( | ||
| "Too many requests were sent from your session. Please try again in a moment." | ||
| ), | ||
| }, | ||
| 500: { | ||
| "title": _("Something went wrong"), | ||
| "message": _( | ||
| "An unexpected error occurred while loading the administration panel." | ||
| ), | ||
| }, | ||
| } | ||
|
|
||
| _ADMIN_ERROR_CODES = (401, 403, 404, 429, 500) | ||
|
|
||
|
|
||
| def _is_administration_request(): | ||
| """Check if the current request targets the administration UI.""" | ||
| admin_ext = current_app.extensions.get("invenio-administration") | ||
| if not admin_ext: | ||
| return False | ||
|
|
||
| admin_url = admin_ext.administration.url.rstrip("/") | ||
| request_path = request.path.rstrip("/") | ||
|
|
||
| if request_path == admin_url: | ||
| return True | ||
|
|
||
| return bool(admin_url and request_path.startswith(f"{admin_url}/")) | ||
|
|
||
|
|
||
| def _render_administration_error(error): | ||
| """Render the administration error page.""" | ||
| status_code = error.code if isinstance(error, HTTPException) else 500 | ||
| content = _ERROR_CONTENT.get(status_code, _ERROR_CONTENT[500]) | ||
|
|
||
| if status_code >= 500: | ||
| current_app.logger.exception("Administration panel error") | ||
|
|
||
| admin_ext = current_app.extensions["invenio-administration"] | ||
| dashboard_endpoint = f"{admin_ext.administration.endpoint}.dashboard" | ||
|
|
||
| return ( | ||
| render_template( | ||
| "invenio_administration/error.html", | ||
| title=content["title"], | ||
| error_code=status_code, | ||
| error_message=content["message"], | ||
| back_url=url_for(dashboard_endpoint), | ||
| ), | ||
| status_code, | ||
| ) | ||
|
|
||
|
|
||
| def _administration_error_or_theme(error, theme_handler): | ||
| """Render admin errors in the admin shell; keep theme handlers elsewhere.""" | ||
| if _is_administration_request(): | ||
| return _render_administration_error(error) | ||
|
|
||
| return theme_handler(error) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in which cases we need this? the error handlder is on the administration blueprint no?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is for safety. This function is called by global error handlers, so it can run on admin and non-admin pages. The check makes sure we show admin error page only on admin URLs. Without it, normal pages could also show admin error layout.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but this is added on the specific blueprint, how is this catching global errors?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not only blueprint level. In this PR we are also registering handlers in ext.py through register_administration_error_handlers(app), and that uses app.register_error_handler(), so it is app wide fallback logic. That means this helper can run outside admin routes too. We keep _administration_error_or_theme() so admin URLs get admin layout and non admin URLs keep theme layout. |
||
|
|
||
|
|
||
| def register_blueprint_error_handlers(blueprint): | ||
| """Register error handlers on the administration blueprint.""" | ||
| # Flask resolves code-specific handlers before generic exception handlers. | ||
| # invenio-theme registers app-level 403/404/etc., so blueprint handlers must | ||
| # be registered per status code to take precedence inside admin views. | ||
| for code in _ADMIN_ERROR_CODES: | ||
| blueprint.register_error_handler(code, _render_administration_error) | ||
|
|
||
| blueprint.register_error_handler(HTTPException, _render_administration_error) | ||
| blueprint.register_error_handler(Exception, _render_administration_error) | ||
|
|
||
|
|
||
| def register_administration_error_handlers(app): | ||
| """Register app-level handlers for admin URLs without a matching view.""" | ||
| app.register_error_handler( | ||
| 401, | ||
| lambda error: _administration_error_or_theme(error, theme_views.unauthorized), | ||
| ) | ||
| app.register_error_handler( | ||
| 403, | ||
| lambda error: _administration_error_or_theme( | ||
| error, theme_views.insufficient_permissions | ||
| ), | ||
| ) | ||
| app.register_error_handler( | ||
| 404, | ||
| lambda error: _administration_error_or_theme(error, theme_views.page_not_found), | ||
| ) | ||
| app.register_error_handler( | ||
| 429, | ||
| lambda error: _administration_error_or_theme( | ||
| error, theme_views.too_many_requests | ||
| ), | ||
| ) | ||
| app.register_error_handler( | ||
| 500, | ||
| lambda error: _administration_error_or_theme(error, theme_views.internal_error), | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| {# | ||
| SPDX-FileCopyrightText: 2026 CERN. | ||
| SPDX-License-Identifier: MIT | ||
| #} | ||
|
|
||
| {% extends config.ADMINISTRATION_BASE_TEMPLATE %} | ||
|
|
||
| {% block page_title %} | ||
| <h1 class="ui header"> | ||
| <i class="bolt icon" aria-hidden="true"></i> | ||
| {{ title }} | ||
| </h1> | ||
| <div class="ui divider" aria-hidden="true"></div> | ||
| {% endblock page_title %} | ||
|
|
||
| {% block admin_page_content %} | ||
| <p>{{ error_message }}</p> | ||
| <a class="ui button primary rel-mt-2" href="{{ back_url }}"> | ||
| {{ _("Back to administration page") }} | ||
| </a> | ||
| {% endblock admin_page_content %} |
Uh oh!
There was an error while loading. Please reload this page.