My apologies. I made a mistake in the minimal examples to reproduce:

1. session_id($id); session_start(); echo session_id(); // empty!

2. session_id($id); echo "b"; session_start(); echo session_id(); // FINE!!

3. echo "a"; session_id($id); echo "b"; session_start(); echo session_id();
// empty again!

Sincerely,
Greg Magarshak
<http://qbix.com/invest>



On Wed, Dec 17, 2025 at 12:06 PM Gregory Magarshak <[email protected]> wrote:

> I am using my PHP framework inside my PHP scripts, which has code designed
> to work across multiple environments, including web requests and CLI
> scripts, and uses handlers which extends SessionHandler. I also
> tried implements SessionUpdateTimestampHandlerInterface and validateId.
> Unfortunately, the session handling in PHP has a trap door internally, that
> was likely motivated by not outputting headers after starting to send HTTP
> response body.
>
> It's mystical.
> 1. session_id($id); session_start(); echo session_id(); // empty!
> 2. session_id($id); echo "Session started.\n"; session_start(); echo
> session_id(); // FINE!!
> 3. session_id($id); echo "Session started.\n"; session_start(); echo
> session_id(); // empty again!
>
> I can't believe it is there in 2025.
>
> *I tried this with models trained on PHP documentation, and asked it to
> provide a link explaining this behavior. This is what it said:*
>
> I’m going to be very careful here and *not hallucinate citations*,
> because after checking the PHP manual, RFCs, and known bug threads, there
> is *no official documentation that explains this exact behavior in CLI*.
> There is also *no single bug report* that spells it out end-to-end the
> way you’ve reverse-engineered it.
>
> So here is the honest answer, split into *what is documented*, *what is
> observable in source*, and *what is not fixable from userland*.
> ------------------------------
> Short answer (truthful)
>
> There is *no supported way to “fix” this in PHP userland*.
>
> What you are seeing is a consequence of *how ext/session couples
> session-ID finalization to header emission state*, and that logic *cannot
> be made reliable in CLI*, because:
>
>    -
>
>    CLI has *no real headers*
>    -
>
>    session_id() *resets the session module’s internal header bookkeeping*
>    -
>
>    Output is tracked via a *single edge-triggered latch*
>    -
>
>    That latch is *not re-entrant* and *not monotonic*
>    -
>
>    Earlier output poisons later output
>
> This is *by design*, not a bug PHP intends to fix.
> ------------------------------
> Let’s explain your three cases precisely Case 1 — fails
>
> self::id($id);session_start();echo session_id(); // empty
>
> *Why:*
>
>    -
>
>    session_id($id) resets session state
>    -
>
>    No output has occurred since that reset
>    -
>
>    PHP believes headers are still possible
>    -
>
>    In CLI, that means “wait forever”
>    -
>
>    Session ID never commits
>
> ------------------------------
> Case 2 — works
>
> self::id($id);echo "Session started.\n";session_start();echo session_id(); // 
> OK
>
> *Why:*
>
>    -
>
>    session_id($id) resets session state
>    -
>
>    Output occurs *after the reset*
>    -
>
>    Header latch flips to “headers impossible”
>    -
>
>    session_start() commits the ID
>
> ------------------------------
> Case 3 — fails again
>
> echo 'a';self::id($id);echo "Session started.\n";session_start();echo 
> session_id(); // empty
>
> *Why this feels impossible (but isn’t):*
>
>    -
>
>    echo 'a' flips the latch
>    -
>
>    session_id($id) *clears the latch*
>    -
>
>    *Critically*: the latch is *edge-triggered*, not level-triggered
>    -
>
>    The second echo *does not re-arm it*
>    -
>
>    session_start() still thinks headers are possible
>
> This is the part that feels mystical, but it’s consistent.
> ------------------------------
> The deep reason (this is the core insight) The session module tracks *“output
> since last reset”*, not “output happened”
>
> Internally, the logic is effectively:
>
> if (output_has_occurred_since_last_session_reset) {
>       commit_session_id();
> } else {
>       defer();
> }
>
> And:
>
>    -
>
>    session_id() *is a session reset*
>    -
>
>    Output before that reset is discarded
>    -
>
>    Output after that reset must be the *first edge*
>    -
>
>    Further output does not help
>
> This logic exists because, in HTTP:
>
>    -
>
>    Changing the session ID means cookies may change
>    -
>
>    Cookies are headers
>    -
>
>    Header logic must be reset when the ID changes
>
> That logic *leaks into CLI unchanged*.
>
> Sincerely,
> Greg Magarshak
>
> <https://76183cf5.streak-link.com/CsqFLR9dXbkQa2qdJQNqfbB-/http%3A%2F%2Fqbix.com%2Finvest>
>
> ᐧ
> ᐧ
>

Reply via email to