Checkout was freezing on the shipping selection step – the spinner kept spinning indefinitely. Customers could not proceed. The problem affected 100% of users on all browsers. Diagnosis time: 2 hours, fix time: 20 minutes.
Symptoms
- The “Shipping” step in checkout never finishes loading
- In DevTools Network: GET
/rest/V1/carts/mine/shipping-methodsreturns 500 - In
var/log/exception.log: PHP error in custom carrier module - Problem started after deploying a new shipping module
Diagnosis
curl -s -H "Authorization: Bearer TOKEN" \ https://shop.com/rest/V1/carts/mine/shipping-methods \ | python3 -m json.tool tail -50 var/log/exception.log | grep -A 10 "shipping" # Disable carriers one by one to isolate the problem bin/magento config:set carriers/custom_carrier/active 0 bin/magento cache:flush
Cause
The new custom carrier’s collectRates() method was calling an external courier API without a timeout. The API was unavailable – PHP waited 30 seconds until nginx terminated the connection. The error was not being logged because the exception was thrown after the nginx timeout.
Solution
<?php
class CustomCarrier extends \Magento\Shipping\Model\Carrier\AbstractCarrier
implements \Magento\Shipping\Model\Carrier\CarrierInterface
{
protected $_code = 'custom_carrier';
public function collectRates(
\Magento\Quote\Model\Quote\Address\RateRequest $request
): ?\Magento\Shipping\Model\Rate\Result {
if (!$this->getConfigFlag('active')) return null;
$result = $this->_rateResultFactory->create();
try {
// Always set timeout for external APIs
$this->_httpClient->setConfig(['timeout' => 5]);
$rates = $this->fetchRatesFromApi($request);
foreach ($rates as $rate) {
$method = $this->_rateMethodFactory->create();
$method->setCarrier($this->_code);
$method->setMethod($rate['code']);
$method->setMethodTitle($rate['name']);
$method->setPrice($rate['price']);
$result->append($method);
}
} catch (\Exception $e) {
// Log error but do not throw - checkout must keep working
$this->_logger->error('Custom carrier API error', [
'exception' => $e->getMessage(),
]);
// Optional: show fallback rate instead of nothing
if ($this->getConfigData('show_fallback')) {
$method = $this->_rateMethodFactory->create();
$method->setCarrier($this->_code);
$method->setMethod('fallback');
$method->setMethodTitle('Standard delivery');
$method->setPrice((float)$this->getConfigData('fallback_price'));
$result->append($method);
}
}
return $result;
}
}
Takeaways
The collectRates() method must be resilient to external API failures. A timeout is mandatory – without it one unavailable carrier blocks the entire checkout. Log errors but do not throw exceptions – the customer should not see a technical error, just a missing shipping option. Consider a fallback rate for critical carriers.
