Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mailer] Sending Messages Async with json serializer does not work #33394

Open
TheRatG opened this issue Aug 30, 2019 · 35 comments
Open

[Mailer] Sending Messages Async with json serializer does not work #33394

TheRatG opened this issue Aug 30, 2019 · 35 comments

Comments

@TheRatG
Copy link

TheRatG commented Aug 30, 2019

Symfony version(s) affected: symfony/mailer: 4.3.4

Description
Sending Messages Async with json serializer does not work with SendEmailMessage.

Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException : Cannot create an instance of Symfony\Component\Mime\RawMessage from serialized data because its constructor requires parameter "message" to be present.
/home/vagrant/www/fxbackoffice.local/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php:505
 ...

How to reproduce

framework:
    messenger:
        serializer:
            default_serializer: messenger.transport.symfony_serializer
            symfony_serializer:
                format: json
        transports:
            async: "%env(MESSENGER_TRANSPORT_DSN)%"

        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage':  async
@TheRatG TheRatG changed the title [Mailer] [Mailer] Sending Messages Async with json serializer does not work Aug 30, 2019
@danielchodusov
Copy link

Iam having the same issue. Sending message async doesn't work. On serialization I recive " A message must have a text or an HTML part or attachments."

@mlojewski
Copy link

Check if your symfony/twig-bundle is in 4.3 version - helped for me

@Mrkisha
Copy link

Mrkisha commented Dec 13, 2019

Having exact same problem, and have symfony/twig-bundle:4.4.

@xabbuh
Copy link
Member

xabbuh commented Dec 14, 2019

Can any of you create a small example application that allows to reproduce the issue?

@diabl0
Copy link

diabl0 commented Dec 20, 2019

Sample app showing error: https://github.com/diabl0/async-mailer

@Nyholm
Copy link
Member

Nyholm commented Jan 26, 2020

I can confirm this issue.

It happens because we cannot normalize RawMessage from Mime component. It has a constructor with required arguments.

We need to use a serializer with the PropertyNormalizer normalizer, then everything will be fine. Im not sure why PropertyNormalizer is not configured by default though...

@wtorsi
Copy link

wtorsi commented Jan 28, 2020

Actually, i've already written about this bug to fabpot, about 6 months ago i hope, but I can't find it now, so i can't say what he said about this problem.
In my app I'm using Mailer in another already async handler.

For quick fix, my decision was to create custom Mailer (thx to container), to exclude redundant de/normalization.

#services/mailer.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    Mailer\Mailer: ~
    Symfony\Component\Mailer\MailerInterface: '@Mailer\Mailer'

# Mailer/Mailer.php
<?php

namespace Mailer;

use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\RawMessage;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class Mailer implements MailerInterface
{
    private TransportInterface $transport;

    public function __construct(TransportInterface $transport)
    {
        $this->transport = $transport;
    }

    public function send(RawMessage $message, Envelope $envelope = null): void
    {
        $this->transport->send($message, $envelope);
    }
}


@TheRatG
Copy link
Author

TheRatG commented Feb 18, 2020

I did an ugly trick with serializer, because SendEmailMessage structure is too complicated for json. The special serializer was added to a project

<?php

namespace Fxbo\Serializer;

use Symfony\Component\Mailer\Messenger\SendEmailMessage;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;

class SendEmailMessageJsonSerializer implements ContextAwareNormalizerInterface, ContextAwareDenormalizerInterface
{
    public function supportsNormalization($data, $format = null, array $context = [])
    {
        return $data instanceof SendEmailMessage && $format === 'json';
    }

    public function normalize($object, $format = null, array $context = [])
    {
        return [__CLASS__ => addslashes(serialize($object))];
    }

    public function supportsDenormalization($data, $type, $format = null, array $context = [])
    {
        return $type == SendEmailMessage::class && $format === 'json' && isset($data[__CLASS__]);
    }

    public function denormalize($data, $type, $format = null, array $context = [])
    {
        return unserialize(stripslashes($data[__CLASS__]));
    }
}

And one more, I did not find how to inject context to serializer for specific class, so there is a file for exclude attributes:

<?php

namespace Fxbo\Serializer;

use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;

class FlattenExceptionSerializer implements ContextAwareNormalizerInterface, ContextAwareDenormalizerInterface
{
    /**
     * @var ObjectNormalizer
     */
    private $normalizer;

    public function __construct(ObjectNormalizer $normalizer)
    {
        $this->normalizer = $normalizer;
    }

    public function supportsNormalization($data, $format = null, array $context = [])
    {
        return $data instanceof FlattenException;
    }

    public function normalize($object, $format = null, array $context = [])
    {
        return $this->normalizer->normalize(
            $object,
            $format,
            array_merge($context, [AbstractObjectNormalizer::IGNORED_ATTRIBUTES => ['previous', 'allPrevious']])
        );
    }

    public function supportsDenormalization($data, $type, $format = null, array $context = [])
    {
        return $type == FlattenException::class;
    }

    public function denormalize($data, $type, $format = null, array $context = [])
    {
        return $this->normalizer->denormalize(
            $data,
            $type,
            $format,
            array_merge($context, [AbstractObjectNormalizer::IGNORED_ATTRIBUTES => ['previous', 'allPrevious']])
        );
    }
}

Firstly I added a specific route and serializer, but there is an error when SendEmailMessage goes to failed queue it decoded as json and this task never be processed successfully.

framework:
    messenger:
        serializer:
            default_serializer: messenger.transport.symfony_serializer
            symfony_serializer:
                format: json
        # after retrying, messages will be sent to the "failed" transport
        failure_transport: failed
        transports:
            mailer:
                dsn: "%env(MESSENGER_TRANSPORT_DSN)%"
                serializer: 'messenger.transport.native_php_serializer'
                options:
                    queue_name: mailer
                    auto_setup: false
            failed:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: failed
                    auto_setup: false
            #...
        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage':  mailer

@airoude
Copy link

airoude commented Jun 30, 2020

@TheRatG creating a seperate transport with the native php serializer worked for me.

@weaverryan
Copy link
Member

Wouldn't adding a getMessage() method to RawMessage fix this? My guess is that the problem is not exactly the constructor argument, but that - due to the missing getMessage() method - the Symfony serializer does not include a message key when serializing. Due to this, when it deserializes, the message key isn't there and deserialization fails. It actually is ok that there is a constructor argument, as long as there is a message key in the JSON that corresponds with the $message constructor argument.

@fabpot fabpot closed this as completed Oct 6, 2020
fabpot added a commit that referenced this issue Oct 6, 2020
This PR was squashed before being merged into the 4.4 branch.

Discussion
----------

[Mime] Fix serialization of RawMessage

| Q             | A
| ------------- | ---
| Branch?       | 4.4 <!-- see below -->
| Bug fix?      | yes
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | Fix #38430, Related #33394 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT
| Doc PR        | - <!-- required for new features -->

The serialization of RawMessage is currently broken if using a generator for message like done by `Symfony\Component\Mailer\SentMessage` see https://github.com/symfony/symfony/blob/5f1c3a797247a6d54992384df00bb22741fc1c34/src/Symfony/Component/Mailer/SentMessage.php#L45
This patch converts the message to a string so further serialization can be done.

This patch probably also solves #33394.

<!--
Replace this notice by a short README for your feature/bugfix. This will help people
understand your PR and can be used as a start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - Never break backward compatibility (see https://symfony.com/bc).
 - Bug fixes must be submitted against the lowest maintained branch where they apply
   (lowest branches are regularly merged to upper ones so they get the fixes too.)
 - Features and deprecations must be submitted against branch 5.x.
-->

Commits
-------

fd99eb2 [Mime] Fix serialization of RawMessage
@stof stof reopened this Oct 6, 2020
@stof
Copy link
Member

stof commented Oct 6, 2020

not actually solved by the related PR, as it was fixing only the case of serialize($rawMessage)

@Wirone
Copy link
Contributor

Wirone commented Mar 3, 2021

Having the same issue with fresh Mailer installation (v5.2.3) and amqp transport. For now I had to set serializer: messenger.transport.native_php_serializer in transport's configuration as a workaround.

Hope it will be fixed, but looking at issue's creation date... 🙄

@Nyholm
Copy link
Member

Nyholm commented Mar 3, 2021

Thank you for reporting the issue still exists. I would be happy to review a PR that fixes this issue.

Im sure that everybody knows that "open source software" means that everybody can help fixing bugs or support the development.

Could someone debug the problem or create a reproducible script (repository) to show this issue?

@Wirone
Copy link
Contributor

Wirone commented Mar 3, 2021

@Nyholm I believe @diabl0's repo is a good starting point, I've just reproduced the issue locally with it. Also I've changed composer.json:

  • "php": "^7.2.5 || ^8.0",
  • changed 5.0.* to ^5.0 in every Symfony package requirement and in extra section

and after composer update issue is still there:

% symfony console messenger:consume async

                                                                                                            
 [OK] Consuming messages from transports "async".                                                           
                                                                                                            

 // The worker will automatically exit once it has received a stop signal via the messenger:stop-workers    
 // command.                                                                                                

 // Quit the worker with CONTROL-C.                                                                         

 // Re-run the command with a -vv option to see logs about consumed messages.                               

[error] Error thrown while handling message Symfony\Component\Mailer\Messenger\SendEmailMessage. Sending for retry #1 using 1000 ms delay. Error: "Cannot send a RawMessage instance without an explicit Envelope."

[critical] Error thrown while running command "messenger:consume async". Message: "Could not decode message: Cannot create an instance of "Symfony\Component\Mime\RawMessage" from serialized data because its constructor requires parameter "message" to be present."


In Serializer.php line 84:
                                                                                                           
  Could not decode message: Cannot create an instance of "Symfony\Component\Mime\RawMessage" from seriali  
  zed data because its constructor requires parameter "message" to be present.                             
                                                                                                           

In AbstractNormalizer.php line 394:
                                                                                                           
  Cannot create an instance of "Symfony\Component\Mime\RawMessage" from serialized data because its const  
  ructor requires parameter "message" to be present.                                                       
                                                                                                           

messenger:consume [-l|--limit LIMIT] [-f|--failure-limit FAILURE-LIMIT] [-m|--memory-limit MEMORY-LIMIT] [-t|--time-limit TIME-LIMIT] [--sleep SLEEP] [-b|--bus BUS] [--] [<receivers>...]

2021-03-03T11:10:36+01:00 [info] User Deprecated: Since symfony/messenger 5.1: Invalid option(s) "queue_name" passed to the AMQP Messenger transport. Passing invalid options is deprecated.
2021-03-03T11:10:36+01:00 [info] User Deprecated: Since symfony/messenger 5.2: Using the "getExceptionMessage()" method of the "Symfony\Component\Messenger\Stamp\RedeliveryStamp" class is deprecated, use the "Symfony\Component\Messenger\Stamp\ErrorDetailsStamp" class instead.
2021-03-03T11:10:36+01:00 [info] User Deprecated: Since symfony/messenger 5.2: Using the "getFlattenException()" method of the "Symfony\Component\Messenger\Stamp\RedeliveryStamp" class is deprecated, use the "Symfony\Component\Messenger\Stamp\ErrorDetailsStamp" class instead.
exit status 1

Unfortunately this repo requires local PHP installation (with amqp extension), but it shows the problem which most important part is in the config/packages/messenger.yamlserializer: messenger.transport.symfony_serializer in the transports' config and Symfony\Component\Mailer\Messenger\SendEmailMessage: async routing.

@Nyholm
Copy link
Member

Nyholm commented Mar 3, 2021

Thank you.

Could you help me understand why 87f3284 wasn't a fix for this problem? Also, do you have any suggestions for what actually will fix the issue?

@Wirone
Copy link
Contributor

Wirone commented Mar 3, 2021

@Nyholm AFAIS 87f3284 is related to native PHP serializer but the problem in this issue relates to JSON serialization. The fact that RawMessage has argument in constructor causes Symfony\Component\Serializer\Normalizer\AbstractNormalizer::instantiateObject() to throw MissingConstructorArgumentsException while consuming message.

For reference, this is payload sent to RabbitMQ with messenger.transport.symfony_serializer serializer when sending email using @diabl0's reproducer:

{"message":{"text":"Sending emails is fun again!","textCharset":"utf-8","html":"<p>See Twig integration for better HTML integration!<\/p>","htmlCharset":"utf-8","attachments":[],"headers":{"from":[{"addresses":[{"address":"hello@example.com","name":""}],"name":"From","lineLength":76,"lang":null,"charset":"utf-8"}],"to":[{"addresses":[{"address":"you@example.com","name":""}],"name":"To","lineLength":76,"lang":null,"charset":"utf-8"}],"subject":[{"value":"Time for Symfony Mailer!","name":"Subject","lineLength":76,"lang":null,"charset":"utf-8"}]},"body":null,"message":null},"envelope":null}

image

I've tested @weaverryan's suggestion and it somehow fixes deserialization issue, but it ends with:

[error] Error thrown while handling message Symfony\Component\Mailer\Messenger\SendEmailMessage. Sending for retry #1 using 1000 ms delay. Error: "Redelivered message from AMQP detected that will be rejected and trigger the retry logic."

[error] Error thrown while handling message Symfony\Component\Mailer\Messenger\SendEmailMessage. Sending for retry #2 using 2000 ms delay. Error: "Cannot send a RawMessage instance without an explicit Envelope."

[error] Error thrown while handling message Symfony\Component\Mailer\Messenger\SendEmailMessage. Sending for retry #3 using 4000 ms delay. Error: "Cannot send a RawMessage instance without an explicit Envelope."

[critical] Error thrown while handling message Symfony\Component\Mailer\Messenger\SendEmailMessage. Removing from transport after 3 retries. Error: "Cannot send a RawMessage instance without an explicit Envelope."

I've just debug it a little and RawMessage's message property was null after deserialization. It seems the root cause of this problem is that Symfony\Component\Mailer\Messenger\SendEmailMessage has constructor argument RawMessage $message while actually Symfony\Component\Mailer\Mailer::send() dispatches SendEmailMessage with Symfony\Component\Mime\Email instance (when using Mailer like docs say). Email is a child class of RawMessage but totally overrides its constructor (in Message parent) and has many other private fields. With RawMessage typing it's impossible to deserialize actual Email.

When I change SendEmailMessage's types from RawMessage to Email whole message is deserialized properly. But I assume it's not the solution, only PoC. However, I think it's now clear what's the problem 😉

@Wirone
Copy link
Contributor

Wirone commented Mar 3, 2021

Actually looking at the SendEmailMessage usage it could be proper solution to change $message's type to Email since it's used only in Symfony\Component\Mailer\Messenger\MessageHandler with:

$this->transport->send($message->getMessage(), $message->getEnvelope())

where Symfony\Component\Mailer\Transport\TransportInterface::send() expects RawMessage which will be OK with Email instance. I've changed it locally (also removed serializer: messenger.transport.native_php_serializer from transport's config, so it uses the default one which is JSON) and it seems to be working 🎉

FYI: Symfony\Component\Mailer\Mailer::send()'s signature must be changed too to ensure proper type for message passed to messenger's message.

If it's acceptable solution I would like to make PR with this change, let my debug time be awarded 😉

@Nyholm
Copy link
Member

Nyholm commented Mar 4, 2021

The SendEmailMessage is used in EmailChannel and in Mailer. Narrowing the constructor parameter would be a BC break. :(

@Nyholm
Copy link
Member

Nyholm commented Mar 4, 2021

This is not a bug in the Mailer, it is an issue in the JsonSerializer.

I'll open a new issue.

@Wirone
Copy link
Contributor

Wirone commented Mar 5, 2021

@Nyholm Personally I am not so sure this is Serializer's issue. It looks to me like architectural mistake in Mailer/Mime component - there is SendEmailMessage message and even its name states it's sending Email, not RawMessage which is used in signature. Not to mention RawMessageMessage inheritance which is weird (totally dropped parent class attributes, not called parent constructor). RawMessage seems to be unnatural abstraction layer to me. Code like this confirms my assumptions:

image

It's from Symfony\Component\Mailer\Envelope, but there are other places with similar workarounds. RawMessage is mainly used for types in signatures while actual usage requires its child classes. Unfortunately it's widely adopted and probably impossible to change at this point.

IMO Mailer/Notifier (and every other component that works with emails) should work with actual Email messages because it's impossible to call Symfony\Component\Mailer\Mailer::send() with RawMessage (well, at least without exlipic Envelope which is not DX-friendly) - it will end up with LogicException regardless of how it's handled internally by Mailer:

Without bus (using TransportInterface):
image

Event dispatcher:
image

Messenger bus:
image

Changing Mailer::send() and SendEmailMessage signatures and using Email would solve this issue and IMO would be generally better since it wouldn't allow developers to get into that LogicException. For now, according to BC-promise, Mailer::send() could simply check if $message is an instance of Email and if not then it should check if $envelope is passed - if not, it could throw exception at this point, without calling transport/dispatcher/bus. It could also trigger deprecation warning and signature could be changed in the next major version. If Mailer::send() can't create Email instance from RawMessage and Envelope, there probably should be DenormalizerInterface implementation for SendEmailMessage object so proper email message would be deserialized.

But it's Symfony's team decision of course 🙂 Maybe it was done this way for a reason I just don't get.

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

@sebo
Copy link

sebo commented Nov 3, 2021

The issue still exists. I had to use the NativePhpSerializer to make it work.

serializer: messenger.transport.native_php_serializer

@xabbuh xabbuh reopened this Nov 7, 2021
@xabbuh xabbuh added Keep open and removed Stalled labels Nov 7, 2021
@florentdestremau
Copy link
Contributor

just had the same problem, found this issue, it very much is still a problem in 5.3

@wouter-toppy
Copy link
Contributor

I have the same issue on 5.3

@florentdestremau
Copy link
Contributor

@xabbuh since you reopened this issue, what are the next steps for this ? We have been using messenger in production for a couple of weeks and being stuck with php-serialization instead of json-based message body is unfortunate. Regarding @Wirone 's comment #33394 (comment) there seem to be a solution but we need a maintainer's input. I would be willing to make the changes if needed.

@mabumusa1
Copy link

@florentdestremau I support your change, we need to use JSON instead of php-serialization

@mabumusa1
Copy link

@florentdestremau please make a PR, I will review it and hopefully, we can get it merged. This is important feature

@florentdestremau
Copy link
Contributor

I have no idea what is needed so unless a contributor is willing to point out the direction, this isn't going anywhere for now

@RCheesley
Copy link

@Nyholm is there any chance of getting some guidance on this issue? It's blocking a pretty major PR we need to integrate in our next major release of Mautic to switch from Swiftmailer to Symfony Mailer but folks don't seem to be clear on how to proceed.

@fabpot
Copy link
Member

fabpot commented Jul 27, 2022

IIUC, there is no issue to fix in Symfony Mailer, but it's more about the inability to serialize an email to JSON via the Symfony serializer.

@Wirone
Copy link
Contributor

Wirone commented Jul 27, 2022

@fabpot I don't know if you're familiar with my comment but IMHO there was architectural issue in Mailer/Mime component. I said "was" because I don't use Messenger/Mailer currently and I don't know state of the codebase. But looking at SendEmailMessage at 6.2 it looks like issue remains.

@kor3k
Copy link
Contributor

kor3k commented Jan 12, 2023

what about widening SendEmailMessage::$message and SendEmailMessage::__construct($message) type to RawMessage|Email ? would that suffice for serializer to handle it properly?

@kor3k
Copy link
Contributor

kor3k commented Jan 12, 2023

@fabpot please, could you explain why is it designed this way ( #33394 (comment) ) ? 🙏
as you are the author of these classes/logic.
understanding it will be a good start to wrap around this issue.
notably

  • the inheritance RawMessage -> Message -> Email
  • most (all?) consumer methods are typed to RawMessage
  • Email is a child class of RawMessage but totally overrides its constructor
  • RawMessage is mainly used for types in signatures

seems to me that RawMessage is kinda substitute for an interface.

fedys added a commit to fedys/mautic that referenced this issue Aug 22, 2023
…symfony/symfony#33394).

Using the combination of JSON and native PHP serialization is not a way as it causes issues with the failed transport.
mautibot pushed a commit to mautic/core-lib that referenced this issue Aug 30, 2023
…symfony/symfony#33394).

Using the combination of JSON and native PHP serialization is not a way as it causes issues with the failed transport.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests