Say which object a Request's attributes and caches belong to - #374
Merged
diolektor merged 1 commit intoSep 20, 2026
Merged
Conversation
Docs: - The attributes container was described as per-request state that is reset with each new request and shared "without using global variables". It lives on the `Request` instance, and `oxphp_http_request()` returns a new instance on every call, so a write made through one call reads back as the default through another — no exception, no warning, no log line, which made the one advertised way to avoid a global the one that silently does not work. The pages now say the container belongs to the object and reaches exactly as far as the object is passed, and show the failing pattern as an anti-example beside the working one. A Fiber is not a special case of either half: handed the object it writes into the same container, calling the factory itself it gets one of its own. - Nothing is reset between requests, because there is nothing to reset — the container goes when the object holding it does. The worker-mode section said the opposite twice: that the previous request's object becomes invalid, and that all caches on it are cleared automatically. A held object is not invalidated and nothing reports it as stale, so the sentence also told a reader they would notice. Its uncached readers go on answering out of whatever request the worker thread is serving at the moment of the call, while each cached item holds whatever it took on its own first call. The section now says that, and points at the list of what the soft reset between requests does cover. - Of the full-array calls only `query()` and `payload()` cache anything; `headers()`, `cookies()` and `files()` build their array afresh on every call. The overview and the parsed-body paragraph both described these as one cache held for the duration of the request, the second of them still doing so after the rest of the page had been corrected. - The same claims stood in `oxphp/stubs`, which is what a user's IDE reads, and in the comment on the factory itself. Tests: - A characterization test pins both halves, so the code cannot quietly become the singleton the old text promised: two factory calls give two objects with two containers, a write through one is invisible through the other by `get()`, `has()` and `all()`, and an object passed along — including into a Fiber — gives one container. 246 tests in the default profile.
diolektor
deleted the
fix/request-api-docs-call-attributes-a-per-request-store
branch
September 20, 2026 13:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The request API documentation described the attributes container as per-request state — created for the request, shared across everything handling it, reset when the next one begins — and offered it as the way to pass data from middleware to a route handler "without using global variables".
The container is a property of one
Requestobject, andoxphp_http_request()ends in a bareobject_init_ex: it builds a newRequeston every call, with a lazily created container of its own. So the pattern the page recommends is the one that does not work:No exception, no warning, no log line — the default comes back. The two code examples on the page both pass
$requestalong and are correct, which is what makes this worth fixing rather than noticing: the copy-pasteable path works, and only the prose around it sends a reader down the other one.Scope
The card that started this named two lines. Grepping for the shape of the claim rather than the symptom turned up eight carriers in four files:
docs/php/request-api.mdattributes(), the attributes paragraph,AttributesInterface,payload(), and two paragraphs under Worker Modeoxphp.stub.phpoxphp/stubs, so it is what a user's IDE readsdocs/php/functions.mdext/oxphp_sapi.cTwo of those are about caching rather than attributes, and were wrong in the same way. Only
query()andpayload()cache anything;headers(),cookies()andfiles()build their array afresh on every call, and there are exactly three cache properties on the object. The overview called them one cache held "for the duration of the request", and the parsed-body paragraph said the same thing aboutpayload()on its own.The Worker Mode paragraph was wrong in the other direction
It said the previous request's object "becomes invalid" and that all caches on it are "cleared automatically". Neither happens, and the first is the more damaging of the two because it tells a reader they would notice.
oxphp_req_is_active()is consulted in exactly one place, the factory. The soft reset between requests does not touch objects, and worker mode has nophp_request_shutdownbetween requests to finalize them. ARequestheld past its handler therefore keeps working:method(),path(),header()and the other uncached readers answer out of whatever request the worker thread is serving at the moment they are called, while each cached item holds whatever it took on its own first call — which, for one never made before, is the current request's data. A framework stashing the object across requests gets a silent mix, not a failure.The section now states that, says plainly that nothing on a
Requestis cleared when the next request begins, and links to the existing list of what the soft reset does cover.What is deliberately not touched
The
### Attributesanchor is unchanged — the superglobals page links to it, and that page's sentence ("for as long as you pass that object along") was already correct. The two code examples are unchanged for the same reason. No signatures move, so no arginfo regeneration.CHANGELOG.mdis untouched: this corrects text that shipped, and the Unreleased section makes no claim about attributes orRequestcaches.Guard
A docs change cannot turn a test red, so there is no red-to-green here and this PR does not claim one. What the new test does is two things.
It was written and run before the prose, as the instrument the prose is written from: every sentence in the new text rests on its output rather than on a reading of the C. Nine assertions — two factory calls return two objects;
attributes()is cached per object; a secondRequestcarries its own container; a write through one call reads back as the default through another byget(),has()andall(); a passed object shares one container; a Fiber handed the object writes into that same container; a Fiber calling the factory itself gets an empty one.And it stays as a regression guard in the other direction: if
oxphp_http_request()ever becomes the singleton the old documentation promised, the first three assertions go red.One premise is not covered by it. The Worker Mode wording above rests on reading the code, not on a test: pinning a
Requestheld across two requests deterministically would need a compose profile with a single-worker pool, which is more machinery than the sentence is worth.Verification
cargo fmt -- --check,cargo clippy --no-default-features -- -D warnings,cargo test --no-default-features— cleanscripts/gen-llms-txt.sh --check— up to date (58 pages);llms-full.txtis regenerated, not hand-edited./tests/run_all.sh --profile=default --no-build— 246 passed, 0 failed, 0 errorsLeft for separate changes
Three things found while reading
payload()andUploadedFile, all out of scope here and filed:Content-Typetable forpayload()is printed twice (the page and the stub) and promises returns the code does not produce. A JSON scalar body is dropped — onlyIS_ARRAYis kept, on both the direct and the fallback decode path — and reads back asnull, indistinguishable from the invalid-JSON case the table lists separately. The stub additionally promises an object, which cannot happen: the decoder is called withPHP_JSON_OBJECT_AS_ARRAY.payload()is gated on the superglobals flag, so withSUPERGLOBALS_ENABLED=falseit returnsnull— while the same page advertises that the object API works regardless. Whether that gate is right is an open question: on the HTTP path the flag demonstrably withholds$_SERVERand the query string, butread_post,read_cookies,content_typeandcontent_lengthare handed over unconditionally and nothing clearsPG(http_globals). Either the "Always available" row is false or the "Empty arrays" row is, and there is no test profile with that flag to say which.UploadedFile::type()is documented as "cached on first call", which is literally true and reads as a per-request cache.file()builds a newUploadedFileevery call, so twotype()calls through it are twomime_content_type()calls — two file reads.