magento / magento2

Prior to making any Submission(s), you must sign an Adobe Contributor License Agreement, available here at: https://opensource.adobe.com/cla.html. All Submissions you make to Adobe Inc. and its affiliates, assigns and subsidiaries (collectively “Adobe”) are subject to the terms of the Adobe Contributor License Agreement.
http://www.magento.com
Open Software License 3.0
11.55k stars 9.32k forks source link

Magento/OfflineShipping/Model/Carrier/Tablerate.php::collectRates is modifying the request for subsequent carriers #38332

Open ioweb-gr opened 10 months ago

ioweb-gr commented 10 months ago

Preconditions and environment

This is a rather technical issue I stumbled upon that I think needs fixing as it is prone to cause bugs.

Steps to reproduce

This is a general issue with the carriers but I happened to stumble on this one while trying to impelment free shipping for a new carrier. The fact that the request wasn't having the correct packageValue on my method worried me so I tried to see the reason behind it. Finally I understood.

Because all variables which refer to objects are passed by reference any setter will actually modify the \Magento\Quote\Model\Quote\Address\RateRequest object for the next carrier invocation.

This is an issue coming from function \Magento\Shipping\Model\Shipping::collectRates

At line

https://github.com/magento/magento2/blob/bfea78834c11594d6d802f4dde0188485fb77274/app/code/Magento/Shipping/Model/Shipping.php#L243C1-L245C14

where as you can see the same request is reused in the foreach loop for all carriers.

Thus each carrier is actually reusing request information that was previously modfied from other carriers' collectRates.

I believe this to be a mistake because if information exists in the \Magento\Quote\Model\Quote\Address\RateRequest that the carrier needs to calculate it's rates, if any other method modifies it, it could affect the results of the upcoming carrier collectRates result.

For my case the

            $request->setPackageValue($newPackageValue);
            $request->setPackageValueWithDiscount($newPackageValue);

Caused weird values to come in the PackageValueWithDisocunt instead of 156 (my order total) I was getting 30.19 due to the disabled method of TableRates

The quick solution would be to pass the object by value instead of by reference.

Is there a reason it's passed by reference instead of by value? I don't know. If any core developer can inform us why it would be great.

I'm thinking that if I changed the value for the items or the weight or anything that could have an effect on rate calculation by mistake in any subsequent method. So it would have significant impact on other carriers.

Expected result

The \Magento\Quote\Model\Quote\Address\RateRequest is provided to each carrier in it's original data

Actual result

The \Magento\Quote\Model\Quote\Address\RateRequest is provided to each carrier modified by each carrier.

Additional information

Let me give a tangible example.

Create a carrier which will internally unset the Free Shipping e.g. $request->setFreeShipping(false).

Then subsequent carriers which rely on $request->getFreeShipping() to set free shipping, will actually fail to set it because the previous carrier removed the information from them.

It happened to me with Amasty's TableRates module which unset the Free Shipping from the Request.

Adding the following patch to module-shipping fixed for me

Index: Model/Shipping.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/Model/Shipping.php b/Model/Shipping.php
--- a/Model/Shipping.php
+++ b/Model/Shipping.php    (date 1704388901162)
@@ -241,7 +241,7 @@
             );

             foreach ($carriers as $carrierCode => $carrierConfig) {
-                $this->collectCarrierRates($carrierCode, $request);
+                $this->collectCarrierRates($carrierCode, clone $request);
             }
         } else {
             if (!is_array($limitCarrier)) {
@@ -256,7 +256,7 @@
                 if (!$carrierConfig) {
                     continue;
                 }
-                $this->collectCarrierRates($carrierCode, $request);
+                $this->collectCarrierRates($carrierCode, clone $request);
             }
         }

Release note

No response

Triage and priority

m2-assistant[bot] commented 10 months ago

Hi @ioweb-gr. Thank you for your report. To speed up processing of this issue, make sure that the issue is reproducible on the vanilla Magento instance following Steps to reproduce. To deploy vanilla Magento instance on our environment, Add a comment to the issue:

m2-assistant[bot] commented 10 months ago

Hi @engcom-November. Thank you for working on this issue. In order to make sure that issue has enough information and ready for development, please read and check the following instruction: :point_down:

engcom-November commented 10 months ago

Hello @ioweb-gr,

Thank you for the report and collaboration!

Verified this issue on 2.4-develop, but in our case it was not reproducible.

Created two carriers namely custom shipping and custom free shipping. In custom shipping carrier, made freeShipping true, and changed the packageValue, but in the custom free shipping carrier's rateRequest we didn't see any of these changes. In our case one carrier did not alter the value in other carrier's rate request.

Please find the attached module and let us know if we are missing anything. Shipping carrier modules.zip

Thank you.

ioweb-gr commented 10 months ago

The modules were loaded in the wrong order so you didn't see the effect.

FreeShipVendor.zip

Try this modified versrion where both carriers after calculation of their amount set free shipping to true for the subsequent invocation.

You'll notice in checkout either one or the other (depending on the internal array pointer) will actually have free shipping offered while they shouldn't.

image

Sorry for the buggy code I didn't get in depth as to why it shows method twice as I only focused on reproducing the free shipping

engcom-November commented 10 months ago

Hello @ioweb-gr,

Thank you for the quick response!

We were able to reproduce the issue with the provided module. Please take a look at the screenshot:

image

freeShipping is set to true in custom free shipping module, but in custom shipping we can see that price is 0, because in here freeShipping has also become true from the other carrier.

Hence confirming the issue.

Thank you.

github-jira-sync-bot commented 10 months ago

:white_check_mark: Jira issue https://jira.corp.adobe.com/browse/AC-10807 is successfully created for this GitHub issue.

m2-assistant[bot] commented 10 months ago

:white_check_mark: Confirmed by @engcom-November. Thank you for verifying the issue.
Issue Available: @engcom-November, You will be automatically unassigned. Contributors/Maintainers can claim this issue to continue. To reclaim and continue work, reassign the ticket to yourself.

ioweb-gr commented 10 months ago

Hi @engcom-November thanks for verifying. I think you should adjust priority on this one. It's a real hidden gem of an issue that has direct impact on the shipping values. I'm pretty sure it has great potential to break a mechants' checkout as it was for us until we found it out. I hope the PR then could be processed a bit faster.