diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py index aae699757..254be41e1 100644 --- a/debug_toolbar/middleware.py +++ b/debug_toolbar/middleware.py @@ -194,7 +194,9 @@ def _postprocess( # included in the response. rendered = toolbar.render_toolbar() - for header, value in self.get_headers(request, toolbar.enabled_panels).items(): + for header, value in self.get_headers( + request, response, toolbar.enabled_panels + ).items(): response.headers[header] = value # Check for responses where the toolbar can't be inserted. @@ -214,12 +216,19 @@ def _postprocess( return response @staticmethod - def get_headers(request: HttpRequest, panels: list[Panel]) -> dict[str, str]: - headers = {} + def get_headers( + request: HttpRequest, response: HttpResponse, panels: list[Panel] + ) -> dict[str, str]: + update, append = {}, {} for panel in panels: - for header, value in panel.get_headers(request).items(): - if header in headers: - headers[header] += f", {value}" - else: - headers[header] = value - return headers + panel_headers = panel.get_headers(request) + update.update(panel_headers.update) + for key, value in panel_headers.append.items(): + append.setdefault(key, response.headers.get(key, "")) + append[key] += f", {value}" + # Only include the append headers when no panel updated the value + # entirely. + for key, value in append.items(): + if key not in update: + update[key] = value + return update diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index 7a95aa522..e97e5b6b8 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass, field + from django.core.handlers.asgi import ASGIRequest from django.template.loader import render_to_string from django.utils.functional import classproperty @@ -7,6 +9,17 @@ from debug_toolbar.utils import get_name_from_obj +@dataclass +class PanelHeaders: + """ + A data-structure to allow panels to update headers or append to existing + values. + """ + + append: dict[str, str] = field(default_factory=dict) + update: dict[str, str] = field(default_factory=dict) + + class Panel: """ Base class for panels. @@ -230,7 +243,7 @@ def process_request(self, request): """ return self.get_response(request) - def get_headers(self, request): + def get_headers(self, request) -> PanelHeaders: """ Get headers the panel needs to set. @@ -254,7 +267,7 @@ def get_headers(self, request): ) for key, record in stats.items() ) - return headers + return PanelHeaders(append=headers) def generate_stats(self, request, response): """ diff --git a/debug_toolbar/panels/history/panel.py b/debug_toolbar/panels/history/panel.py index e6d4f576a..c0bba5788 100644 --- a/debug_toolbar/panels/history/panel.py +++ b/debug_toolbar/panels/history/panel.py @@ -9,7 +9,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from debug_toolbar.panels import Panel +from debug_toolbar.panels import Panel, PanelHeaders from debug_toolbar.panels.history import views from debug_toolbar.panels.history.forms import HistoryStoreForm @@ -22,12 +22,12 @@ class HistoryPanel(Panel): nav_title = _("History") template = "debug_toolbar/panels/history.html" - def get_headers(self, request: HttpRequest) -> dict: - headers: dict = super().get_headers(request) + def get_headers(self, request: HttpRequest) -> PanelHeaders: + headers: PanelHeaders = super().get_headers(request) observe_request = self.toolbar.get_observe_request() request_id = self.toolbar.request_id if request_id and observe_request(request): - headers["djdt-request-id"] = request_id + headers.update["djdt-request-id"] = request_id return headers @property diff --git a/docs/changes.rst b/docs/changes.rst index 118fbde7c..367f4f0dd 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,7 @@ Change log Pending ------- +* Fix Server-Timing header not being overridden if it exists in header already * Replaced ``requirements_dev.txt`` file for ``pyproject.toml`` support with dependency groups. * Updated ReadTheDocs Python version to 3.13. diff --git a/tests/test_integration.py b/tests/test_integration.py index 43a5fbc0b..3fe3925aa 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -626,6 +626,21 @@ def test_server_timing_headers(self): for expected in expected_partials: self.assertTrue(re.compile(expected).search(server_timing)) + def test_server_timing_headers_with_existing_value(self): + response = self.client.get("/server-timing/") + server_timing = response["Server-Timing"] + expected_partials = [ + 'existing_key;dur=100;desc="Details"', + r'TimerPanel_utime;dur=(\d)*(\.(\d)*)?;desc="User CPU time", ', + r'TimerPanel_stime;dur=(\d)*(\.(\d)*)?;desc="System CPU time", ', + r'TimerPanel_total;dur=(\d)*(\.(\d)*)?;desc="Total CPU time", ', + r'TimerPanel_total_time;dur=(\d)*(\.(\d)*)?;desc="Elapsed time", ', + r'SQLPanel_sql_time;dur=(\d)*(\.(\d)*)?;desc="SQL 1 queries", ', + r'CachePanel_total_time;dur=0;desc="Cache 0 Calls"', + ] + for expected in expected_partials: + self.assertTrue(re.compile(expected).search(server_timing)) + @override_settings(DEBUG_TOOLBAR_CONFIG={"RENDER_PANELS": True}) def test_timer_panel(self): response = self.client.get("/regular/basic/") diff --git a/tests/urls.py b/tests/urls.py index 32355d110..f39f7a979 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -30,6 +30,7 @@ path("ajax/", views.ajax_view), path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)), path("csp_view/", views.csp_view), + path("server-timing/", views.server_timing), path("admin/", admin.site.urls), path("__debug__/", include("debug_toolbar.urls")), ] diff --git a/tests/views.py b/tests/views.py index 5d3a51ffb..d79ff78d1 100644 --- a/tests/views.py +++ b/tests/views.py @@ -125,3 +125,9 @@ def redirect_view(request): def ajax_view(request): return render(request, "ajax/ajax.html") + + +def server_timing(request): + response = execute_sql(request) + response.headers["Server-Timing"] = 'existing_key;dur=100;desc="Details"' + return response