Magento 2 Shipping Rates: Unpacking a Persistent PHP 8 TypeError in `collectRates()`

Magento 2 Shipping Rates: Unpacking a Persistent PHP 8 TypeError in collectRates()

As an e-commerce migration expert at Shopping Mover, we often encounter intricate issues within Magento 2's core functionality, especially when clients are upgrading to newer PHP versions or optimizing their store configurations. A recent GitHub issue (magento/magento2#41182) sheds light on a critical TypeError within Magento's shipping rate collection process that can significantly disrupt the checkout experience for merchants and customers alike.

The issue, automatically created based on an existing pull request, highlights a vulnerability in Magento\Shipping\Model\Shipping::collectRates(). This crucial method is responsible for gathering available shipping options. The problem arises specifically when no shipping carriers are configured or enabled within the Magento instance. In such scenarios, the _scopeConfig->getValue('carriers', ...) call returns null instead of an empty array. On PHP 8 and later, attempting to iterate over a null value using a foreach loop immediately throws a TypeError, effectively breaking the checkout process.

The problematic code snippet is as follows:

$carriers = $this->_scopeConfig->getValue(
    'carriers',
    \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
    $storeId
);

foreach ($carriers as $carrierCode => $carrierConfig) {

This behavior stems from Magento\Config\App\Config\Type\System::getDataByPathParts(), which returns null when a requested configuration key is absent. Since ScopeConfigInterface::getValue() is declared to return mixed, callers are expected to handle non-array return types gracefully. The issue description points out an asymmetry: a few lines below in the same method, the $limitCarrier branch already correctly normalizes its value with is_array() before iteration, suggesting an oversight in the main branch.

What makes this bug particularly noteworthy is its historical context. This isn't an entirely new problem but rather the 'remaining half' of a defect reported back in 2021 (magento/magento2#30830). That previous report detailed the same failure ('disable all shipping carrier modules' → blank checkout, array_keys(): argument #1 must be of type array, null given). It was partially fixed by #30822, which guarded identical getValue('carriers', ...) calls in Magento\Shipping\Model\Config::getActiveCarriers() and getAllCarriers(), but crucially missed the one in Shipping::collectRates(). This indicates a persistent pattern of incomplete fixes or a need for more comprehensive code reviews, especially concerning PHP version compatibility.

The proposed solution is straightforward: add a check to ensure $carriers is an array before attempting to iterate over it. This simple guard prevents the TypeError, allowing rate collection to complete and return no rates gracefully, as expected when no carriers are configured. Manual testing scenarios confirm that disabling all shipping carriers, then proceeding to checkout, reproduces the TypeError before the fix and results in normal completion (without rates) after the fix.

Interestingly, the community interaction on this issue includes a comment from engcom-Bravo stating they were 'not able to reproduce the issue in Latest 2.4-develop instance'. This could imply that the bug might have been inadvertently resolved in a later snapshot of the 2.4-develop branch, or that the reproduction steps require a very specific environment. However, the detailed description of the underlying logic flaw and its historical recurrence suggests that the potential for this TypeError remains, particularly for stores on slightly older 2.4.x versions or those undergoing complex module uninstallation processes. For developers and merchants, understanding this potential pitfall is crucial for maintaining a stable Magento 2 environment, especially when migrating or updating their PHP versions.

Start with the tools

Explore migration tools

See options, compare methods, and pick the path that fits your store.

Explore migration tools