datasette/datasette/handle_exception.py
Matt Van Horn 5f83d94119
Add content hash to JS includes (closes #2714)
The CSS link in base.html already carries `?{{ app_css_hash }}` so that
browsers refetch when the bundled file changes. The five first-party JS
files shipped with datasette did not. Cache-busting JS the same way
matches the existing CSS pattern and uses the static_hash() helper that
already powers app_css_hash().

Files updated:
- datasette/app.py: expose static_hash as a callable in template context.
- datasette/handle_exception.py: include static_hash in the error-page
  template context (mirrors the existing app_css_hash entry there).
- datasette/templates/base.html: hash datasette-manager.js and
  navigation-search.js.
- datasette/templates/table.html: hash column-chooser.js, table.js, and
  mobile-column-actions.js.
- tests/test_html.py: new test_js_content_hash parametrized across all
  five files; existing test_navigation_menu_links updated to expect the
  new query string.

Vendored libraries (cm-editor-6.0.1.bundle.js, sql-formatter-2.3.3.min.js,
json-format-highlight-1.0.1.js) already carry a version in the filename
and were left unchanged.
2026-05-26 00:14:15 -07:00

78 lines
2.2 KiB
Python

from datasette import hookimpl, Response
from .utils import add_cors_headers
from .utils.asgi import (
Base400,
)
from .views.base import DatasetteError
from markupsafe import Markup
import traceback
try:
import ipdb as pdb
except ImportError:
import pdb
try:
import rich
except ImportError:
rich = None
@hookimpl(trylast=True)
def handle_exception(datasette, request, exception):
async def inner():
if datasette.pdb:
pdb.post_mortem(exception.__traceback__)
if rich is not None:
rich.get_console().print_exception(show_locals=True)
title = None
if isinstance(exception, Base400):
status = exception.status
info = {}
message = exception.args[0]
elif isinstance(exception, DatasetteError):
status = exception.status
info = exception.error_dict
message = exception.message
if exception.message_is_html:
message = Markup(message)
title = exception.title
else:
status = 500
info = {}
message = str(exception)
traceback.print_exc()
templates = [f"{status}.html", "error.html"]
info.update(
{
"ok": False,
"error": message,
"status": status,
"title": title,
}
)
headers = {}
if datasette.cors:
add_cors_headers(headers)
if request.path.split("?")[0].endswith(".json"):
return Response.json(info, status=status, headers=headers)
else:
environment = datasette.get_jinja_environment(request)
template = environment.select_template(templates)
return Response.html(
await template.render_async(
dict(
info,
urls=datasette.urls,
app_css_hash=datasette.app_css_hash(),
static_hash=datasette.static_hash,
menu_links=lambda: [],
)
),
status=status,
headers=headers,
)
return inner