Skip to content

Read the profiler cookie by the name the documentation shows - #369

Merged
diolektor merged 1 commit into
mainfrom
fix/profiler-cookie-trigger-unreachable-under-plugin-prefix
Sep 19, 2026
Merged

diolektor merged 1 commit into
mainfrom
fix/profiler-cookie-trigger-unreachable-under-plugin-prefix

Conversation

@diolektor

Copy link
Copy Markdown
Contributor

What was wrong

Of the three explicit profiler triggers, the one the documentation recommends for debugging in a browser was the only one that could never fire.

trigger.rs asked for the cookie through PluginRequestView::cookie, which reads the per-plugin cookie namespace. That namespace is built by extract_plugin_cookies, which keeps only cookies prefixed __oxp_profiler_ and hands them over with the prefix removed. So the only spelling that ever reached the trigger was __oxp_profiler_OXPROF — a name documented nowhere. The bare OXPROF that the README and every page of the profiling documentation have printed since 0.3.0 was filtered out before the profiler saw the request.

From the outside this is indistinguishable from a server with profiling switched off: no error, no log line, runs_total{source="cookie"} simply stays at zero. The end-to-end suite did cover the trigger, but it sent the namespaced spelling, so it was a false green on a dead path. The unit tests were green for the same reason from the other side: the fixture constructed PluginCookies directly and never ran the parse that contained the defect.

Why the bare name is canonical

The documented behaviour is the reasonable one; the implementation was the broken half. The bare name is what an operator reads and types, it is published in three places, and it has been there for five releases. The namespaced spelling is documented nowhere and used by nothing except our own suite line. So the code moves to the documentation, not the other way round.

Rejected: documenting the prefixed name (it makes the published examples wrong for every existing reader and exposes an internal naming convention as a public interface), and accepting both names (two spellings for one knob, one of which nobody can discover, with no way to ever retire it).

global_cookie is pub(crate), deliberately. Cookie isolation stays fully in force for anything built against the public plugin API, and the public header() still refuses cookie; the exception is available only to code in this crate, for a cookie whose whole name is part of an interface an operator types by hand.

The HTTP/2 half

find_raw_cookie reads every Cookie field line, not the first.

RFC 9113 section 8.2.3 allows an HTTP/2 client to split its cookies across several Cookie field lines for better HPACK compression, and puts the job of joining them back with "; " on the server "before being passed into a non-HTTP/2 context, such as … a generic HTTP server application". Nothing below us does it: h2 0.4.17 passes the field lines through as they arrived, hyper 1.10 does not rejoin them, and HeaderMap::get returns only the head value of a multi-valued entry. The server speaks h2 both by ALPN and by prior knowledge on a plaintext port.

Without this, the fix would still have missed the cookie in precisely the case it exists for — a browser over HTTP/2 — and the miss would again have looked like a disabled profiler. Measured on a live h2 connection against the pre-fix build: cookies split across two field lines gave expected: true, actual: false; after the fix one, two and three field lines all activate, in any order, and the namespaced spelling in a later field line stays inert.

Scope boundary: extract_plugin_cookies and strip_plugin_cookies still read the first field line only. That gap predates this change and is not closed here — it is tracked separately, because closing it touches the plugin cookie contract and the SAPI's $_COOKIE construction, both well outside this fix.

Behaviour worth knowing before merging

This cookie, unlike an __oxp_* one, is not stripped from the request. The token stays visible to the application being profiled and to anything that logs or forwards cookies — in $_COOKIE when the client sent its cookies on a single field line, and in $_SERVER['HTTP_COOKIE'] when it split them, because those two superglobals are built from different field lines. That is measured, not inferred. The documentation already advises against putting a production token on this path; §5.2 now says why in one sentence, and the changelog entry repeats it.

Proof

  • Unit RED observed on the unfixed code after rebuilding the fixture on a real Cookie header: left: None, right: Some("tok"), five tests red.
  • End-to-end RED on the pre-fix image with the suite line corrected: profiler/test_source_cookie fails on is_active() === false.
  • GREEN: profiler profile 25/25 on a rebuilt image (image identity verified by digest, not by assumption — the obvious string marker was absent from both builds and proved nothing).
  • Premise mutants killed: reversing the field-line scan order, and filter_mapmap_while on the unreadable-line skip. A third mutant showed a trim() call was doing nothing and it was removed.

Verification

  • cargo fmt -- --check — clean
  • cargo clippy --all-targets --no-default-features -- -D warnings — clean
  • cargo test --no-default-features --features plugin-apm,plugin-async,plugin-profiler,plugin-shared — 1716 passed, 0 failed
  • scripts/gen-llms-txt.sh --check — up to date (58 pages)
  • ./tests/run_all.sh --profile=profiler — 25/25

Fix:
  - Look the `OXPROF` cookie up by its whole name instead of through the per-plugin cookie namespace. That namespace admits only cookies prefixed `__oxp_profiler_` and hands them over with the prefix removed, so the only spelling that ever reached the trigger was `__oxp_profiler_OXPROF`, a name documented nowhere. The bare `OXPROF` printed in the README and on every page of the profiling documentation since 0.3.0 was filtered out before the profiler saw the request, so of the three explicit triggers the one recommended for debugging in a browser was the only one that could not fire, and an operator who set it from DevTools got a server that behaved exactly as if profiling were switched off. The namespaced spelling stops working, which costs nothing to anyone following the documentation and is pinned by a test so it is not restored by accident. The new read is `pub(crate)`: cookie isolation stays in force for everything built against the public plugin API, and the public `header()` still refuses `cookie`.
  - Read every `Cookie` field line rather than the first. RFC 9113 section 8.2.3 lets an HTTP/2 client split its cookies across several field lines for HPACK efficiency and puts the job of joining them back together on the server; neither `h2` nor `hyper` does it, and `HeaderMap::get` returns only the head value. The server speaks h2 both by ALPN and by prior knowledge on a plaintext port, so without this the fix would still have missed the cookie in precisely the case it exists for — a browser over HTTP/2 — and the miss is indistinguishable from a disabled profiler. Verified against a live h2 connection: on the previous build two field lines gave `expected: true, actual: false`. `extract_plugin_cookies` and `strip_plugin_cookies` still read the first field line only, so this function sees cookies they do not; that gap predates this change and is not closed here.

Tests:
  - Rebuild the trigger fixture on a real `Cookie` header. It used to construct `PluginCookies` directly, skipping the parse that contained the defect, which is why a dead trigger sat behind green unit tests; on the unfixed code the rewritten fixture turns five of them red. The suite probe now sends the documented spelling — sending the namespaced one made the only end-to-end coverage of this trigger a false green.

Docs:
  - Drop the migration note promising that `runs_total{source="cookie"}` stays at zero, and record that this cookie, unlike an `__oxp_*` one, is not stripped: the token reaches the application, in `$_COOKIE` or in `$_SERVER['HTTP_COOKIE']` depending on how the client sent its cookies.

1716 unit tests, 25 profiler-profile tests.
@diolektor
diolektor merged commit 67493a9 into main Sep 19, 2026
7 checks passed
@diolektor
diolektor deleted the fix/profiler-cookie-trigger-unreachable-under-plugin-prefix branch September 19, 2026 21:50
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.

1 participant