Skip to content

Lowercase-stored HTTP methods are unreachable from the endpoint method filter #321

Description

@FrameAutomata

Split out of #313 so that issue can close with #319. This is the half of #313 that is not a defect in the code as written — it is an open product decision.

Why it is not a regression

#313 framed the method filter as having "silently become case-sensitive" in c7a5dcae. That is not what happened:

embedded (SQLite/DuckDB) ClickHouse
before #290 no method filter no method filter
#290 (89a3e4dc) LIKE 'GET %' — case-insensitive startsWith(endpoint, 'GET ') — case-sensitive
c7a5dcae SUBSTR(...) = 'GET ' — case-sensitive unchanged

The ~24 hours between #290 and c7a5dcae were the parity break; c7a5dcae closed it by aligning the embedded backends onto ClickHouse. It also shipped TestEndpointRepository_FindGroupedByEndpoint_MethodFilter, which inserts get /api/lowercase and asserts it is excluded — the exclusion is deliberate and tested. SUBSTR over LIKE is independently worth keeping: methodFilter is user input and LIKE would let % and _ through as wildcards.

The actual problem

Lowercase-stored methods cannot be reached by any filter option.

getHTTPEndpoint (backend/app/controllers/otelcontrollers/trace_converter.go:462) concatenates http.request.method verbatim with no ToUpper:

method := getStringAttribute(attrs, "http.request.method")
if method == "" {
    method = getStringAttribute(attrs, "http.method")
}
...
return method + " " + route

So get /api/orders rows really can exist. Meanwhile METHOD_OPTIONS (frontend/src/lib/components/ui/endpoint-filter/methods.ts) offers only the 7 canonical values, which normalizeMethodFilter uppercases. No dropdown selection can ever match such a row — it is visible in the unfiltered list and vanishes under every filter.

Options

  1. Leave it. RFC 9110 says method tokens are case-sensitive, and the list already groups get /x and GET /x as two separate endpoints, so the filter treating them as distinct is internally consistent.
  2. Case-insensitive filterUPPER(SUBSTR(...)) = :method on the embedded backends, startsWith(upper(...), ?) on ClickHouse. Keeps the no-wildcard property, matches how search already behaves in the same clause builder (INSTR(LOWER(...), LOWER(:search))), and reaches rows already stored. Costs the deliberate test above.
  3. Normalize at ingestToUpper the method in getHTTPEndpoint. Fixes the cause rather than compensating at query time in three SQL dialects, but does not reach rows already stored, and merges two currently-distinct endpoint groups.

Option 3 is the one I'd pick — it addresses the source — with the caveat that existing rows stay unreachable unless paired with 2 or a backfill.

Whichever is chosen, all three backends must move together or this becomes a parity break again.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions