fix(ui): percent-encode request id in spend log detail URL (#48630)

* test: cover request ids with URL special characters in log detail fetches

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): percent-encode request id in spend log detail URL

Co-authored-by: Charlie Freeman <243496084+cf3901646@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Charlie Freeman <243496084+cf3901646@users.noreply.github.com>
This commit is contained in:
moe-berri 2026-10-27 13:33:47 -07:00 • committed by GitHub
parent 9e01b3c37b
commit 34c10829b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 43 additions and 3 deletions

View file

@ -15,6 +15,7 @@ from datetime import datetime, timezone
from pathlib import Path
from types import MappingProxyType
from typing import Final
from urllib.parse import quote
import click
import requests
@ -177,7 +178,7 @@ class SpendLogsFetcher:
return DebugFailure(f"Unexpected {uri} response: {e}")
def payload(self, request_id: str) -> RequestResponsePayload | None | DebugFailure:
uri: Final = f"/spend/logs/ui/{request_id}"
uri: Final = f"/spend/logs/ui/{quote(request_id, safe='')}"
raw: Final = self._get(uri)
if isinstance(raw, DebugFailure):
return raw

View file

@ -1,6 +1,8 @@
import json
import os
import re
import time
from urllib.parse import unquote
import pytest
import requests
@ -114,6 +116,22 @@ def test_recent_bodies_fetches_latest_turns_even_when_successful():
assert _called_paths() == ["/spend/logs/session/ui", "/spend/logs/ui/req-ok"]
@pytest.mark.parametrize("request_id", ["req#a1", "req?a=1", "req%41b", "req a&b"])
@responses.activate
def test_bodies_are_fetched_for_request_ids_with_url_special_characters(request_id):
def payload_for_stored_id(request):
stored_id = unquote(request.path_url.split("?")[0].removeprefix("/spend/logs/ui/"))
found = stored_id == request_id
return 200, {}, json.dumps({"response": {"id": "msg_special"}} if found else None)
_mock_proxy([{**OK_ROW, "request_id": request_id}], {})
responses.add_callback(responses.GET, re.compile(f"{PROXY}/spend/logs/ui/.*"), callback=payload_for_stored_id)
result = CliRunner().invoke(cli, ["debug", "claude", "--session-id", SESSION, "--no-save"])
assert result.exit_code == 0, result.output
assert "msg_special" in result.output
@responses.activate
def test_bodies_are_truncated_to_max_chars():
_mock_proxy([OK_ROW], {"req-ok": {"proxy_server_request": {"body": "a" * 5000}, "response": None}})

View file

@ -602,6 +602,27 @@ describe("uiSpendLogsCall exclude_internal_health_checks serialization", () => {
});
});
describe("uiSpendLogDetailsCall", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it.each(["chatcmpl-123#a1", "chatcmpl-123?a=1", "chatcmpl-123%41b", "chatcmpl-123 a&b"])(
"requests the details of %s with the full request id and start date",
async (logId) => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ id: logId }) });
vi.stubGlobal("fetch", mockFetch);
await Networking.uiSpendLogDetailsCall("token", logId, "2026-01-01 00:00:00");
const url = new URL(String(mockFetch.mock.calls.at(-1)?.[0]), "http://example.com");
expect(decodeURIComponent(url.pathname.split("/spend/logs/ui/")[1])).toBe(logId);
expect(url.hash).toBe("");
expect([...url.searchParams.entries()]).toEqual([["start_date", "2026-01-01 00:00:00"]]);
},
);
});
describe("sessionSpendLogsCall", () => {
const originalFetch = global.fetch;

View file

@ -4709,8 +4709,8 @@ export const uiSpendLogDetailsCall = async (accessToken: string, logId: string,
try {
// Construct base URL
let url = proxyBaseUrl
? `${proxyBaseUrl}/spend/logs/ui/${logId}?start_date=${encodeURIComponent(start_date)}`
: `/spend/logs/ui/${logId}?start_date=${encodeURIComponent(start_date)}`;
? `${proxyBaseUrl}/spend/logs/ui/${encodeURIComponent(logId)}?start_date=${encodeURIComponent(start_date)}`
: `/spend/logs/ui/${encodeURIComponent(logId)}?start_date=${encodeURIComponent(start_date)}`;
const response = await fetch(url, {
method: "GET",