Description of the bug
With page caching and background fetch both enabled, serving an expired cache entry produces two PHP warnings on every affected request:
Warning: session_set_save_handler(): Session save handler cannot be changed after headers have already been sent
in backdrop_session_initialize() (line 306 of core/includes/session.inc).
Warning: session_id(): Session ID cannot be changed after headers have already been sent
in backdrop_session_initialize() (line 331 of core/includes/session.inc).
_backdrop_bootstrap_page_cache() serves the stale page, calls ob_end_flush() and fastcgi_finish_request(), sets backdrop_is_background(TRUE) and lets the request continue to regenerate the entry. The bootstrap then proceeds to BACKDROP_BOOTSTRAP_SESSION, where backdrop_session_initialize() calls session_set_save_handler() and session_id() — with headers already sent.
Instrumenting backdrop_session_initialize() with headers_sent($file, $line) confirms it, on every occurrence:
[example.com] /some/path background=1 → headers sent from core/includes/bootstrap.inc:3277
That line is the ob_end_flush() in _backdrop_bootstrap_page_cache().
Steps To Reproduce
- Enable page caching and background fetch, and set page cache expiry to 1 minute (admin/config/development/performance).
- Request a URL anonymously, using a unique query string so no other traffic can consume the trigger:
URL="https://example.com/?cachetest=$RANDOM"
curl -s -o /dev/null -D - "$URL" | grep -i x-backdrop-cache # MISS
curl -s -o /dev/null -D - "$URL" | grep -i x-backdrop-cache # HIT
sleep 70
curl -s -o /dev/null -D - "$URL" | grep -i x-backdrop-cache # HIT — stale, triggers this
The third request is the one that reproduces it. With background fetch disabled, the third request returns MISS and no warnings occur.
Why this is probably under-reported
The SESSION bootstrap phase runs before module_load_all(), so watchdog() is a no-op at that point and the warnings are usually discarded. They only reach dblog on sites that happen to have loaded bootstrap modules earlier — for example when $settings['page_cache_invoke_hooks'] = TRUE is set, which calls module_load_all(TRUE) during the page cache phase.
In other words, the warnings are raised on every affected request, but most sites never see them logged. I only found them because one of my sites has that setting.
Actual behavior
Expected behavior
No warning
Additional information
Environment: PHP-FPM (fpm-fcgi), fastcgi_finish_request() available, output_buffering = 4096, zlib.output_compression off. Reproduced on several sites with different module sets. Backdrop 1.34.3
Proposed fix
During a background regeneration, the response has already been sent, so there is no session to initialize for this request:
function backdrop_session_initialize() {
global $user, $is_https;
if (backdrop_is_background()) {
$user = backdrop_anonymous_user();
date_default_timezone_set(backdrop_get_user_timezone());
return;
}
// ...
}
A narrower alternative would be to guard only the two offending calls, or to check headers_sent() instead — backdrop_is_background() expresses the intent better, but you are better placed than me to judge which is correct here.
Description of the bug
With page caching and background fetch both enabled, serving an expired cache entry produces two PHP warnings on every affected request:
_backdrop_bootstrap_page_cache() serves the stale page, calls ob_end_flush() and fastcgi_finish_request(), sets backdrop_is_background(TRUE) and lets the request continue to regenerate the entry. The bootstrap then proceeds to BACKDROP_BOOTSTRAP_SESSION, where backdrop_session_initialize() calls session_set_save_handler() and session_id() — with headers already sent.
Instrumenting backdrop_session_initialize() with headers_sent($file, $line) confirms it, on every occurrence:
That line is the ob_end_flush() in _backdrop_bootstrap_page_cache().
Steps To Reproduce
The third request is the one that reproduces it. With background fetch disabled, the third request returns MISS and no warnings occur.
Why this is probably under-reported
The SESSION bootstrap phase runs before module_load_all(), so watchdog() is a no-op at that point and the warnings are usually discarded. They only reach dblog on sites that happen to have loaded bootstrap modules earlier — for example when $settings['page_cache_invoke_hooks'] = TRUE is set, which calls module_load_all(TRUE) during the page cache phase.
In other words, the warnings are raised on every affected request, but most sites never see them logged. I only found them because one of my sites has that setting.
Actual behavior
Expected behavior
No warning
Additional information
Environment: PHP-FPM (fpm-fcgi), fastcgi_finish_request() available, output_buffering = 4096, zlib.output_compression off. Reproduced on several sites with different module sets. Backdrop 1.34.3
Proposed fix
During a background regeneration, the response has already been sent, so there is no session to initialize for this request:
A narrower alternative would be to guard only the two offending calls, or to check headers_sent() instead — backdrop_is_background() expresses the intent better, but you are better placed than me to judge which is correct here.