symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
820 stars 297 forks source link

[Live component] Invalid cheksum exception with own hydrating methods after upgrade from 2.10 to 2.11 #2000

Closed sabat24 closed 1 month ago

sabat24 commented 2 months ago

I have got a Live Component which contains a LiveProp defined as array of DTO with custom hydrateWith and dehydrateWith methods.

/** @var CourseMeetingResponse[] $courseMeetings */
#[LiveProp(writable: true, hydrateWith: 'hydrateCourseMeetings', dehydrateWith: 'dehydrateCourseMeetings')]

In 2.10. everything works fine, but after upgrading to 2.11 or 2.12. or 2.13. I started to receive a Hydration exception Invalid checksum sent when updating the live component.

I saw that in 2.11 you have changed the checksum calculation. However before it works fine for me and now the new calculation causes exception. I didn't find any BC in changelog or any information about action which is required after upgrading to newer versions.

What should be done to avoid such behaviour?

smnandre commented 2 months ago

It's a bit old for me to remember but one thing that could be related is we removed the hard dependency on symfony/serializer ... it is possible you don't have it anymore and this prevent something during hydration ?

sabat24 commented 2 months ago

@smnandre I was referring directly to that change: Fix checksum calculation for deeply nested data.

I found that commit. I upgraded ux-live-component to 2.11.0 (with reinstalling js files) and reverted back change made by that commit. So using again ksort fixed my problem.

smnandre commented 2 months ago

Could you give us a (simplified) set of data to understand the duffzrence for you ? And see if and how we can find a fix ?

sabat24 commented 2 months ago

First array is sorted by ksort and everything works fine. Second array is sorted by new method and breaks a checksum calculation. As you can see the order of associative keys in sub-arrays has changed.

obraz

Below is a json data from my POST request to my component.

{"props":{"title":"Dostępne terminy","courseMeetings":[{"id":7199,"variantCode":"symbol_243188","realizationDate":{"date":"2024-05-10 10:00:00.000000","timezone_type":3,"timezone":"UTC"},"placeName":"szkolenie online","earlyClubMembershipPrice":85000,"clubMembershipPrice":90000,"earlyNonClubMembershipPrice":90000,"nonClubMembershipPrice":100000,"consultantId":1388},{"id":7200,"variantCode":"symbol_243195","realizationDate":{"date":"2024-05-19 00:00:00.000000","timezone_type":3,"timezone":"UTC"},"placeName":"szkolenie online","earlyClubMembershipPrice":85000,"clubMembershipPrice":90000,"earlyNonClubMembershipPrice":90000,"nonClubMembershipPrice":100000,"consultantId":1388},{"id":7201,"variantCode":"symbol_243196","realizationDate":{"date":"2024-05-22 12:00:00.000000","timezone_type":3,"timezone":"UTC"},"placeName":"Gdańsk","earlyClubMembershipPrice":85000,"clubMembershipPrice":90000,"earlyNonClubMembershipPrice":90000,"nonClubMembershipPrice":100000,"consultantId":1350},{"id":7202,"variantCode":"symbol_243197","realizationDate":{"date":"2024-05-29 17:20:00.000000","timezone_type":3,"timezone":"UTC"},"placeName":"Sopot","earlyClubMembershipPrice":85000,"clubMembershipPrice":90000,"earlyNonClubMembershipPrice":90000,"nonClubMembershipPrice":100000,"consultantId":1388},{"id":7203,"variantCode":"symbol_243206","realizationDate":{"date":"2024-06-05 12:00:00.000000","timezone_type":3,"timezone":"UTC"},"placeName":"szkolenie online","earlyClubMembershipPrice":85000,"clubMembershipPrice":90000,"earlyNonClubMembershipPrice":90000,"nonClubMembershipPrice":100000,"consultantId":1412}],"itemsErrors":[],"formName":"add_to_cart","add_to_cart":{"courseMeeting":"symbol_243188","addToCartSubmit":null},"isValidated":false,"validatedFields":[],"@attributes":{"data-live-id":"live-962431554-0"},"@checksum":"Xo5RMRMvZz4DMff3vSPadkxqC32GnD5nDTStuTVSiGg="},"updated":{"add_to_cart.courseMeeting":"symbol_243206","validatedFields":["add_to_cart.courseMeeting"]}}

As you can see the json data sent to component differs from data recursively sorted by new method. Is it possible that something wasn't updated on a JS part?

obraz

smnandre commented 2 months ago

That should not have impact, as POST data is also recursively sorted before we compute and compare checksum.

Just a inch with no certitude at all: could you try without the field "realizationDate" ?

WebMamba commented 2 months ago

Hey @sabat24! Could you create a basic reproducer? So we can look at your issue, I didn't manage to reproduce it locally. Thanks for the report!

sabat24 commented 2 months ago

Just a inch with no certitude at all: could you try without the field "realizationDate" ?

@smnandre You were right. It seems that this field causes problem since 2.11. When I removed it from my component, checksum exception disappeared. I will dig into it.

smnandre commented 2 months ago

Ok good to know and tell us if you cannot sort this out.

by curiosity what was the différence between both array representations ?

sabat24 commented 2 months ago

In 2.11 when component was mounted and dehydrate method called the calculateChecksum method my realizationDate field was a DateTime object.

obraz

Then after liveAction was triggered the verifyChecksum method was called and my realizationDate field was an array.

obraz

In 2.10 when component was mounted the behaviour was same as above. But when live action was triggered the dump method in calculateChecksum was called twice.

obraz

On the first call from verifyChecksum method my realizationDate was represented as an array. But on the second call from dehydrate method my realizationDate was a DateTime object.

Regardless of that second call, using ksort in 2.11 instead of $this->recursiveKeySort works fine. So I guess that recursiveKeySort may sorts the dehydrated DateTime object during verification, but do not during creation?

@WebMamba I can try to create a reproducer in a few days.

sabat24 commented 2 months ago

I created repo to reproduce the issue -> https://github.com/sabat24/symfony-ux-2000 I installed 2.12 version of twig and live component, but it doesn't matter. Live component is defined in App\Order\LiveComponent\OrderComponent

After installing just go to home page and select radio button.

obraz

smnandre commented 2 months ago

Some things, not sure if one particular solves all

        $courseMeetings = [];
        foreach ($data as $courseMeeting) {
            /** @var array{id: int, variantCode: string, realizationDate: array{date: string}, placeName: string, earlyClubMembershipPrice: string|null, clubMembershipPrice: string|null, earlyNonClubMembershipPrice: string|null, nonClubMembershipPrice: string|null, consultantId: int|null} $courseMeetingArray */
            $courseMeetingArray = (array) $courseMeeting;
            $courseMeetings[] = $courseMeetingArray;
        }

This is false, (array) $courseMeeting does not transform realizationDate into an array, it's still a DateTime (you can dd($courseMeetingArray) to check)

--

Your mock data uses strings for date, not your hydrate/dehydrate methods after that

"realizationDate":"2024-07-26 00:00:00"

--

I updated twig & live packages (in 2.18) and installed Serializer (not required since 2.something) ..

Then i removed your hydrate/dehydrate and things seems to "work".

Maybe start doing the same and see where it goes for you ?

sabat24 commented 2 months ago

I think that I know where the "problem" was.

In short in my dehydration method as you mentioned.

  1. Mock is a data provider which sends $realizationDate = "2024-07-26 00:00:00".
  2. SF serializer converts string into DateTime object.
  3. LiveComponent use my dehydrate method and then calculates checksum based on DateTime object.
  4. LiveComponent saves this DateTime object internally in props as an array "realizationDate":{"date":"2024-07-18 00:00:00.000000","timezone_type":3,"timezone":"UTC"}
  5. When model changes and LiveAction calls checksum verification an array (from point 4) is send to calculate checksum.

Here the checksum exception is raised.

  1. If checksum is valid LiveComponent tries to hydrate back the array into object and then my hydrate method converts an array realizationDate into DateTime object.

If i modify my dehydration method to

$courseMeetings = [];
        foreach ($data as $courseMeeting) {
            /** @var array{id: int, variantCode: string, realizationDate: array{date: string}, placeName: string, earlyClubMembershipPrice: string|null, clubMembershipPrice: string|null, earlyNonClubMembershipPrice: string|null, nonClubMembershipPrice: string|null, consultantId: int|null} $courseMeetingArray */
            $courseMeetingArray = (array) $courseMeeting;
            $courseMeetingArray['realizationDate'] = (array) $courseMeetingArray['realizationDate']; // <-- added
            $courseMeetings[] = $courseMeetingArray;
        }

everything works fine.

I was sure that LiveComponent calculates checksum based on props from point 4 and not from point 3.

smnandre commented 2 months ago

everything works fine.

Really happy for you! 😃