diff --git a/api/routes.py b/api/routes.py index 18d9c1a5..ecd7912a 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2629,9 +2629,13 @@ def handle_get(handler, parsed) -> bool: if parsed.path.startswith("/api/kanban/"): from api.kanban_bridge import handle_kanban_get - if handle_kanban_get(handler, parsed): - return True - return _kanban_unknown_endpoint(handler, parsed, "GET") + # Only treat an explicit False as "no route matched". None means the + # bridge already sent a response via bad()/j() — emitting our own 404 + # on top of that produces concatenated JSON bodies on the wire. + result = handle_kanban_get(handler, parsed) + if result is False: + return _kanban_unknown_endpoint(handler, parsed, "GET") + return True if parsed.path == "/api/wiki/status": return _handle_llm_wiki_status(handler, parsed) if parsed.path == "/api/logs": @@ -3429,9 +3433,10 @@ def handle_post(handler, parsed) -> bool: if parsed.path.startswith("/api/kanban/"): from api.kanban_bridge import handle_kanban_post - if handle_kanban_post(handler, parsed, body): - return True - return _kanban_unknown_endpoint(handler, parsed, "POST") + result = handle_kanban_post(handler, parsed, body) + if result is False: + return _kanban_unknown_endpoint(handler, parsed, "POST") + return True if parsed.path == "/api/dashboard/config": from api import dashboard_probe @@ -4607,9 +4612,10 @@ def handle_patch(handler, parsed) -> bool: if parsed.path.startswith("/api/kanban/"): from api.kanban_bridge import handle_kanban_patch - if handle_kanban_patch(handler, parsed, body): - return True - return _kanban_unknown_endpoint(handler, parsed, "PATCH") + result = handle_kanban_patch(handler, parsed, body) + if result is False: + return _kanban_unknown_endpoint(handler, parsed, "PATCH") + return True return False @@ -4621,9 +4627,10 @@ def handle_delete(handler, parsed) -> bool: if parsed.path.startswith("/api/kanban/"): from api.kanban_bridge import handle_kanban_delete - if handle_kanban_delete(handler, parsed, body): - return True - return _kanban_unknown_endpoint(handler, parsed, "DELETE") + result = handle_kanban_delete(handler, parsed, body) + if result is False: + return _kanban_unknown_endpoint(handler, parsed, "DELETE") + return True return False # ── GET route helpers ───────────────────────────────────────────────────────── diff --git a/tests/test_issue1823_kanban_not_found.py b/tests/test_issue1823_kanban_not_found.py index 6899994b..d9fdd3e2 100644 --- a/tests/test_issue1823_kanban_not_found.py +++ b/tests/test_issue1823_kanban_not_found.py @@ -67,6 +67,31 @@ def test_kanban_stale_client_error_renders_hard_refresh_escape_hatch(): assert "window.location.reload()" in PANELS +def test_inner_handler_bad_response_does_not_emit_double_404(monkeypatch): + """Regression: when the kanban bridge already sent a response via bad() + (returns None), the unknown-endpoint wrapper must not concatenate a second + 404 body on the wire. Only an explicit `False` from the bridge means the + path was unmatched. + """ + from api import kanban_bridge + + # Force the task-log payload helper to report "not found" so the bridge + # calls bad() and returns None. + monkeypatch.setattr(kanban_bridge, "_task_log_payload", lambda *a, **kw: None) + + handler = _FakeHandler() + handled = routes.handle_get(handler, urlparse("/api/kanban/tasks/abc/log")) + + assert handled is True + assert handler.status == 404 + body = handler.wfile.getvalue().decode("utf-8") + # Exactly one JSON object should have been written. Two concatenated + # objects would produce something like `}{` between them. + assert body.count("}{") == 0, f"double response detected: {body!r}" + payload = json.loads(body) + assert payload["error"] == "task not found" + + def test_kanban_load_resolves_board_before_board_scoped_requests(): boards_pos = PANELS.find("await loadKanbanBoards();") config_pos = PANELS.find("api('/api/kanban/config' + _kanbanBoardQuery())")