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