Skip to content

feat: Add post_response event that gets triggered after response is sent to the client#10423

Open
patel-vansh wants to merge 7 commits into
codeigniter4:4.8from
patel-vansh:feat/post-response
Open

feat: Add post_response event that gets triggered after response is sent to the client#10423
patel-vansh wants to merge 7 commits into
codeigniter4:4.8from
patel-vansh:feat/post-response

Conversation

@patel-vansh

Copy link
Copy Markdown
Contributor

Description
This PR adds a new post_response event that is triggered from the run() method of the CodeIgniter class after the response has been sent to the client.

The ResponseTrait::send() method now manually finalizes the request lifecycle after sending the response. The implementation of ResponseTrait::finishResponse() and ResponseTrait::closeOutputBuffers() is adapted from Symfony's implementation, which can be viewed here:

This allows developers to register post-response tasks, such as cleanup operations or analytics processing, without delaying the response sent to the client.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@github-actions github-actions Bot added the 4.8 PRs that target the `4.8` branch. label Jul 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new lifecycle event, post_response, intended for running post-response work after the framework has sent the HTTP response to the client, enabling tasks like cleanup or analytics without delaying the response.

Changes:

  • Trigger a new post_response event from CodeIgniter::run() after sendResponse() completes.
  • Update ResponseTrait::send() to finalize the response lifecycle (FastCGI/LiteSpeed finish request or output buffer flushing) before continuing execution.
  • Add system test coverage and user guide/changelog documentation for the new event.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
user_guide_src/source/extending/events.rst Documents the new post_response event and its intent.
user_guide_src/source/changelogs/v4.8.0.rst Adds a changelog entry for the new event (minor formatting nit noted).
tests/system/CodeIgniterTest.php Adds a system test asserting post_response fires after the response is sent.
tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php Adds a test response subclass to detect when send() has completed.
system/HTTP/ResponseTrait.php Finalizes/flushed response output after sending headers/cookies/body.
system/CodeIgniter.php Triggers post_response after sending the response.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- **Filters:** Added ``RequestId`` filter for request tracing and correlation logging. The filter stores the request ID in the request context and automatically adds the ``X-Request-ID`` response header. Incoming ``X-Request-ID`` headers are used when valid. See :ref:`requestid` for details.
- **Environment:** Added ``CodeIgniter\EnvironmentDetector`` class and corresponding ``environment`` service as a mockable wrapper around the ``ENVIRONMENT`` constant.
Framework internals that previously compared ``ENVIRONMENT`` directly now go through this service, making environment-specific branches reachable in tests via ``Services::injectMock()``. See :ref:`environment-detector-service`.
- **Events**: Added ``post_response`` event to run after the response is sent to the client.
@michalsn

Copy link
Copy Markdown
Member

Copilot acted rather poorly here, so I asked Claude for the review:

Problems

  1. send() gets a silent, unconditional behavior change. Every Response::send() call
    under FPM/LiteSpeed/FrankenPHP now terminates the request — including
    ExceptionHandler (system/Debug/ExceptionHandler.php:95) and any app code calling
    send() manually. Anything echoed after send() is silently lost. The changelog only
    lists the event under "Others"; no "Behavior changes" entry. Symfony hit exactly this
    and added an opt-out (send(bool $flush = true)); this PR offers none.

  2. The promise doesn't hold for DownloadResponse and SSEResponse. Both override
    send() and never call finishResponse(), yet post_response still fires for them.
    Under FPM the connection stays open until script end, so a slow listener after a large
    download makes the client wait — the opposite of what the docs promise.

  3. Only truly "post-response" on FPM/LiteSpeed/FrankenPHP. On Apache mod_php and
    spark serve (cli-server), the fallback just closes buffers and flush()es — the
    connection stays open until script end, so listeners still delay the response. Same
    limitation as Symfony, but the docs claim "without delaying the response"
    unconditionally. Needs a SAPI note.

  4. Session lock footgun. Symfony saves the session before sending
    (AbstractSessionListener); CI4 releases it at script shutdown. A long post_response
    task holds the session lock and blocks that user's next request. At minimum a docs
    warning; closing the session before the trigger would be the real fix but is a scope
    decision.

  5. Error responses never fire the event. Throwables escape run()
    (system/CodeIgniter.php:285) and ExceptionHandler sends directly, so
    post_response doesn't fire for error pages (unlike Symfony's terminate). Defensible,
    but should be a documented decision, not an accident.

  6. finishResponse() ignores $this->pretend. Pretend mode means "don't touch real
    output," but it would still call fastcgi_finish_request(). Tests run under CLI so CI
    won't catch it; guard it anyway.

Recommendation

Move the finishResponse() call out of ResponseTrait::send() and into
CodeIgniter::sendResponse() (as a public finish()-style method on the response, called
right before the trigger). That single change fixes most of the above:

  • send() keeps its current contract for direct callers and ExceptionHandler (fixes 1, 6);
  • downloads/SSE get finished too, since their overridden send() is no longer the hook
    point (fixes 2).

Then add: a behavior-change note in the changelog, a SAPI-support note plus session-lock
warning in the events docs, and a decision on error-path semantics.

Minor: public $responseSent in ResponseWithPostSendFlag is untyped; the
use (&$response) reference in the test closure is unnecessary.

@paulbalandan paulbalandan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this should be 2 PRs: one for the event and one for the response finalization. If, however, it should be really be one PR then the title should be rephrased accurately.

$level = count($status);
$flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE;

while ($level-- > 0 && ($s = $status[$level]) && ($s['del'] ?? (! isset($s['flags']) || ($s['flags'] & $flags) === $flags))) {

@paulbalandan paulbalandan Jul 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you destructure/rewrite this whole conditional line to be more readable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants