Skip to content

Pass the handler the message it was registered for, not the envelope - #352

Merged
KalimeroMK merged 1 commit into
yiisoft:masterfrom
KalimeroMK:fix/handler-receives-message
Nov 1, 2026
Merged

KalimeroMK merged 1 commit into
yiisoft:masterfrom
KalimeroMK:fix/handler-receives-message

Conversation

@KalimeroMK

@KalimeroMK KalimeroMK commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor
Q A
Is bugfix? ✔️
New feature? ❌
Breaks BC? ❌
Fixed issues #341
Tests added? ✔️

What does this PR do?

A handler is registered for a message type, and the guide writes it that way
(docs/guide/en/messages-and-handlers.md):

public function handle(MessageInterface $message): void
{
    assert($message instanceof SendEmailMessage);
    $this->mailer->send($message->to, $message->subject, $message->body);
}

Nothing unwrapped an envelope between the adapter and that handler, so an assigned id — the
normal asynchronous path — broke it:

$worker->process(new IdEnvelope($message, 'id-42'), 'test');

With zend.assertions=1:

MessageFailureException: Processing of message #id-42 is stopped because of an exception:
assert($message instanceof SendEmailMessage).

With zend.assertions=-1, the production default, there is no error at all — the properties
read as null and the work is done against empty values. DelayEnvelope and FailureEnvelope
behave the same way.

Fix

ConsumeFinalHandler unwraps before calling the handler, carrying the envelope's meta over to
the message:

if ($message instanceof Envelope) {
    $message = $message->getMessage()->withMeta($message->getMeta());
}

The meta matters. Envelope::__construct() keeps the merged meta on the envelope while
$this->message stays the innermost original, whose own getMeta() never gets the envelope's
keys — so a plain getMessage() would hand the handler a message with yii-id, yii-delay and
yii-failure gone, and IdEnvelope::fromMessage($message)->getId() would start returning null
for anything that logs or de-duplicates on the id. Carried over, those reads keep working.

Unwrapping is the last step of the pipeline, so consume middleware still receive the envelope
itself. Envelope::getMessage() is final and already returns the innermost message — the
constructor flattens — so a stack of envelopes needs no loop here.

Two tests: the handler receives the message with the envelope's meta intact, and a consume
middleware still sees the envelope while the handler does not.

Note on #338

ConsumeFinalHandler is @internal and the change is one line, but #338 reworks Worker and the
pipeline around it. If that lands first, the same unwrap belongs wherever the final handler ends
up — happy to follow up either way.

Note on #342

InMemoryAdapter stores the unwrapped message, which is why the stub never showed this. Storing
the envelope there, as #342 asks, is safe once this is in.

Summary by Sourcery

Pass the registered message, with its envelope metadata preserved, to the final handler while retaining envelopes for consume middleware.

Bug Fixes:

  • Ensure handlers receive the registered message rather than an adapter-created envelope, while preserving envelope metadata such as IDs, delays, and failure counts.
  • Keep consume middleware receiving the original envelope before the final handler unwraps it.

Documentation:

  • Document that handlers receive the registered message with envelope metadata, while consume middleware continues to receive the envelope.

Tests:

  • Add coverage verifying handlers receive unwrapped messages with preserved metadata and consume middleware still sees envelopes.

A handler is registered for a message type, and the guide writes it with
`assert($message instanceof SendEmailMessage)`. Nothing unwrapped an envelope
on the way, so as soon as an adapter returned the message wrapped — an
assigned id is the normal asynchronous path — the handler got the wrapper.
With `zend.assertions=-1`, the production default, there is no error either:
the properties read as `null` and the work is done against empty values.

`ConsumeFinalHandler` now unwraps before calling the handler, carrying the
envelope's meta over to the message. An envelope keeps the merged meta on
itself while the message it wraps keeps its own, so unwrapping alone would
lose the id, the delay and the failure counts. Unwrapping is the last step of
the pipeline, so consume middleware still receive the envelope itself.
@sourcery-ai

sourcery-ai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The final consume handler now unwraps adapter-created envelopes before invoking registered handlers, carries merged envelope metadata onto the underlying message, and leaves consume middleware envelope-aware; documentation, changelog, and regression tests cover the behavior.

Sequence diagram for envelope unwrapping before message handling

sequenceDiagram
    participant Adapter
    participant Middleware as ConsumeMiddleware
    participant FinalHandler as ConsumeFinalHandler
    participant Handler
    participant Envelope
    participant Message

    Adapter->>Middleware: handleConsume(request with Envelope)
    Middleware->>FinalHandler: handleConsume(request with Envelope)
    FinalHandler->>Envelope: getMessage()
    Envelope-->>FinalHandler: Message
    FinalHandler->>Envelope: getMeta()
    Envelope-->>FinalHandler: merged metadata
    FinalHandler->>Message: withMeta(merged metadata)
    Message-->>FinalHandler: Message with metadata
    FinalHandler->>Handler: handler(Message)
    Handler-->>Handler: Read message payload and metadata
Loading

File-Level Changes

Change Details Files
Unwrap envelopes at the final handler boundary while preserving envelope metadata on the original message.
  • Detect envelope-wrapped input and replace it with the innermost message using the envelope’s merged metadata.
  • Keep consume middleware positioned before unwrapping so it continues to receive the envelope.
  • Document that handlers receive registered message types and retain envelope metadata.
src/Middleware/Consume/ConsumeFinalHandler.php
docs/guide/en/messages-and-handlers.md
Add regression coverage for handler input and middleware ordering.
  • Verify handlers receive the original message rather than an IdEnvelope, with payload, type, and id metadata intact.
  • Verify consume middleware sees the envelope while the final handler receives the unwrapped message.
tests/Unit/WorkerTest.php
Record the bug fix in the changelog.
  • Describe envelope unwrapping, metadata preservation, and middleware behavior.
CHANGELOG.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Nov 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.26%. Comparing base (e3c4c92) to head (aaac28a).

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #352      +/-   ##
============================================
+ Coverage     95.15%   95.26%   +0.11%     
- Complexity      395      396       +1     
============================================
  Files            59       59              
  Lines          1031     1034       +3     
============================================
+ Hits            981      985       +4     
+ Misses           50       49       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sourcery-ai sourcery-ai Bot 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.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@KalimeroMK
KalimeroMK merged commit cbd3661 into yiisoft:master Nov 1, 2026
35 of 37 checks passed
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