Skip to content

feat(dbal): replay dead letter messages to a chosen channel - #726

Merged
dgafka merged 1 commit into
mainfrom
dgafka/reply-to-dlq
Oct 20, 2026
Merged

dgafka merged 1 commit into
mainfrom
dgafka/reply-to-dlq

Conversation

@dgafka

@dgafka dgafka commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Why is this change proposed?

Important

Replaying to a channel that does not exist is refused before any message is replayed or deleted.

Dead letter messages can now be replayed to a channel the operator picks, instead of only the channel they were polled from.

Until now a replay always went back through polledChannelName, inboundRequestChannel or the stored routing slip. A message that kept failing on its source channel could only be retried through the code that had just failed on it. A message with none of those headers could not be replayed at all.

Description of Changes

  • DeadLetterGateway::replay() and replayAll() take an optional target channel. Without it, behaviour is unchanged.
  • ecotone:deadletter:replay and ecotone:deadletter:replayAll take the same target as a --targetChannel option.
  • With a target, the stored routing slip is dropped and the message goes only to the target.
  • An unknown target throws an InvalidArgumentException that names the channel and lists what is valid, before any message is touched.

Before / after

Replay with Before After
no target back to the source channel unchanged
a target channel not possible goes to the target, then leaves the dead letter
an unknown target not possible refused up front, dead letter untouched
a message with no source headers refused replayable when given a target

Usage

The target is any channel the application has: an #[InternalHandler] input channel, an asynchronous channel, or a command handler's routing key.

use Ecotone\Api\Attribute\InternalHandler;

final class OrderRecovery
{
    #[InternalHandler('orders.recover')]
    public function recover(PlaceOrder $command): void
    {
    }
}

Symfony

use Ecotone\Api\Dbal\Gateway\DeadLetterGateway;

final class DeadLetterController
{
    public function __construct(private DeadLetterGateway $deadLetter) {}

    public function recover(string $messageId): Response
    {
        $this->deadLetter->replay($messageId, 'orders.recover');

        return new Response();
    }
}
bin/console ecotone:deadletter:replay 6f1c3a2e-... --targetChannel=orders.recover
bin/console ecotone:deadletter:replayAll --targetChannel=orders.recover

Laravel

use Ecotone\Api\Dbal\Gateway\DeadLetterGateway;

final class DeadLetterController
{
    public function recover(DeadLetterGateway $deadLetter, string $messageId): Response
    {
        $deadLetter->replay($messageId, 'orders.recover');

        return response()->noContent();
    }
}
php artisan ecotone:deadletter:replay 6f1c3a2e-... --targetChannel=orders.recover
php artisan ecotone:deadletter:replayAll --targetChannel=orders.recover

Tempest

use Ecotone\Api\Dbal\Gateway\DeadLetterGateway;
use Tempest\Router\Post;

final readonly class DeadLetterController
{
    public function __construct(private DeadLetterGateway $deadLetter) {}

    #[Post('/dead-letter/{messageId}/recover')]
    public function recover(string $messageId): Response
    {
        $this->deadLetter->replay($messageId, 'orders.recover');

        return new Ok();
    }
}
./tempest ecotone:deadletter:replay 6f1c3a2e-... --targetChannel=orders.recover
./tempest ecotone:deadletter:replayAll --targetChannel=orders.recover

Without --targetChannel (or with a null target on the gateway) each message goes back to its source channel, exactly as before.

Pull Request Contribution Terms

  • I have read and agree to the contribution terms outlined in CONTRIBUTING.

@dgafka
dgafka merged commit 9e3d01e into main Oct 20, 2026
15 checks passed
@dgafka
dgafka deleted the dgafka/reply-to-dlq branch October 20, 2026 18:54
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.

1 participant