Skip to content

fix: make the client-traffic trace answer the question it exists for (#120) - #122

Merged
ifahimreza merged 1 commit into
mainfrom
fix/120-trace-answers-the-question
Aug 18, 2026
Merged

fix: make the client-traffic trace answer the question it exists for (#120)#122
ifahimreza merged 1 commit into
mainfrom
fix/120-trace-answers-the-question

Conversation

@ifahimreza

Copy link
Copy Markdown
Contributor

Closes #120

Found by reading a customer's capture of a failing ChatGPT connection, not by reading the code. Three defects, all of which shaped the last two weeks of that thread.

1. The panel recorded its own polling

targets_mcp() was a bare strpos() prefix test, and Saddle_REST_Admin::REST_NAMESPACE is also saddle/v1 — so /saddle/v1/mcp-diagnostics matched, and McpDiagnostics.jsx polls it every 5 seconds while recording.

At MAX_ENTRIES = 25 the ring buffer therefore turned over every ~125 seconds. The capture that arrived spanned 114 seconds and held two real rows among twenty-three of the panel watching itself. The instrument was evicting the evidence it exists to collect.

Matched exactly now (=== $mcp || strpos( $route, $mcp . '/' ) === 0), buffer raised to 100 for margin.

2. The credential scheme was recorded and rendered nowhere

Every row has carried Saddle_Connection::credential_scheme() since the recorder shipped. Neither report()'s format string nor the table printed it.

So the one field that separates "the key was rejected" from "no key arrived" — the same 401, opposite fixes, one is reconnect the app and the other is your host is stripping the Authorization header — was written to the option and shown to nobody. The customer asked us for it by name, after the answer was already sitting in saddle_mcp_trace on his own site.

It is now first on the row, ahead of session and protocol, which are trivia next to it. Paired with an explicit auth: present|absent|unknown, because '' and 'unknown' are different answers and neither is "present".

3. No HTTP method

A row reading Asked for — was ambiguous between a GET, a POST with the wrong content type, and an empty body. Different answers, identical row. method is recorded now.

A test that was passing for the wrong reason

test_a_recorded_request_contains_no_credential() set an Authorization header on a synthetic WP_REST_Request, which populates no $_SERVER — and Saddle_Connection reads $_SERVER deliberately, because the failure this surface exists to diagnose is a host stripping the header before PHP. So the recorder never saw a credential and the "no leak" assertion was vacuous.

The helper now sets both. That is what makes the two new scheme assertions meaningful, and it retroactively proves the original assertion was load-bearing.

Testing

  • composer test599 tests (was 596), 2003 assertions, 1 pre-existing skip, green
  • Each new test verified red without its fix. One of them was not, first time round: the targets_mcp regression passed against the bug, because rest_do_request() never fires rest_post_dispatch and so nothing was written either way. Rewritten to drive the real chain, and it now fails correctly without the fix. Left a comment saying so.
  • composer lint — 0 errors (pre-existing warnings only); npm run lint:js clean
  • npm run build — bundle rebuilt and committed
  • languages/saddle.pot regenerated

Three defects, all found by reading a customer's capture of a failing
ChatGPT connection rather than by reading the code.

The panel recorded its own polling. targets_mcp() was a bare strpos()
prefix test and the admin API shares the saddle/v1 namespace, so
/saddle/v1/mcp-diagnostics matched — and the panel polls it every five
seconds while recording. At 25 entries the ring buffer therefore turned
over every ~125 seconds. The capture that arrived spanned 114 seconds and
held two real rows among twenty-three of the panel watching itself. The
instrument was evicting the evidence it exists to collect. Matched
exactly now, and the buffer raised to 100 for margin.

The credential scheme was recorded and rendered nowhere. Every row has
carried Saddle_Connection::credential_scheme() since the recorder
shipped; neither report() nor the table printed it. So the one field that
distinguishes "the key was rejected" from "no key arrived" — a 401 either
way, and opposite fixes — was written to the option and shown to no one,
and a customer had to ask us what his own trace already knew. Now first
on the row, ahead of session and protocol, which are trivia by
comparison. Paired with an explicit auth present/absent/unknown, because
'' and 'unknown' are different answers and neither is "present".

And the HTTP method is recorded, so a row carrying no MCP method is no
longer ambiguous between a GET, a POST with the wrong content type, and
an empty body.

The credential-leak test was passing for the wrong reason: it set an
Authorization header on a synthetic WP_REST_Request, which populates no
$_SERVER, so the recorder never saw a credential to leak. The helper now
sets both, which is what made the two new scheme assertions meaningful —
and what proves the existing assertion was load-bearing rather than
vacuous.

Closes #120
@ifahimreza
ifahimreza merged commit ddd82c0 into main Aug 18, 2026
1 of 7 checks passed
@ifahimreza
ifahimreza deleted the fix/120-trace-answers-the-question branch August 18, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The client-traffic recorder floods its own trace and can't name the credential scheme

1 participant