Skip to content

Introduce a dedicated ResponseEmitter class to safely handle responses#64

Merged
tommitchelmore merged 4 commits intomasterfrom
fix/headers-already-sent-emitter-exception
Mar 24, 2026
Merged

Introduce a dedicated ResponseEmitter class to safely handle responses#64
tommitchelmore merged 4 commits intomasterfrom
fix/headers-already-sent-emitter-exception

Conversation

@tommitchelmore
Copy link
Copy Markdown
Collaborator

@tommitchelmore tommitchelmore commented Mar 24, 2026

Safe Response Emission and Defensive Header Management

Overview

This PR introduces a ResponseEmitter abstraction to handle PSR-7 response emission safely and implements defensive guards across the core to prevent "headers already sent" issues during complex WordPress lifecycles.

Problem

  1. Emitter Exceptions: The Laminas\HttpHandlerRunner\Emitter\SapiEmitter throws an EmitterException if headers_sent() is true, which occurs frequently in WordPress when plugins or the theme have already started output.
  2. Duplicate Header Modification: Certain actions like send_headers can be triggered multiple times, and attempting to modify headers (e.g., setting session cookies or removing headers) after output has started leads to PHP warnings and unstable state.

Solution

Safe Response Emission

We introduced Rareloop\Lumberjack\Http\ResponseEmitter, which encapsulates emission logic with a fallback mechanism:

  • Direct Echo: If headers_sent() is true, it echoes the response body directly.
  • SapiEmitter Delegation: If headers are still modifiable, it delegates to Laminas\SapiEmitter for standard emission.

Defensive Header Management

  • src/Application.php: Added a headers_sent() guard to removeSentHeadersAndMoveIntoResponse. This ensures the method returns early if it's too late to manipulate the header stack, avoiding unnecessary processing and potential side effects.
  • src/Providers/SessionServiceProvider.php: Implemented a $cookieSet state lock and a headers_sent() check within the send_headers action. This prevents duplicate session cookies and ensures setcookie() is only called when valid, specifically handling cases where WordPress might fire the hook twice.

Implementation Details

  • src/Http/ResponseEmitter.php: New class providing the safe emit() method.
  • src/Application.php: Refactored to use ResponseEmitter and added a terminate() wrapper for testing.
  • src/Bootstrappers/RegisterExceptionHandler.php: Updated to use the new emitter via dependency injection.
  • src/Providers/SessionServiceProvider.php: Hardened the session cookie emission logic.

Testing

  • ResponseEmitter Tests: Used phpmock to verify both "headers sent" and "headers not sent" scenarios.
  • Application & Bootstrapper Tests: Updated to reflect the new dependency injection and delegation logic.
  • Manual Verification: Confirmed that the "headers already sent" warnings are suppressed and that response bodies are still delivered successfully when output has already started.

… emission and refactor core classes to use it via dependency injection
@tommitchelmore tommitchelmore self-assigned this Mar 24, 2026
@tommitchelmore tommitchelmore marked this pull request as ready for review March 24, 2026 14:01
@tommitchelmore tommitchelmore merged commit d96270b into master Mar 24, 2026
12 checks passed
@tommitchelmore tommitchelmore deleted the fix/headers-already-sent-emitter-exception branch March 24, 2026 15:26
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.

2 participants