{"id":19026187,"date":"2026-09-22T11:25:59","date_gmt":"2026-09-22T11:25:59","guid":{"rendered":"https:\/\/wordpress.org\/support\/topic\/infinite-template-recursion-in-form-billing-php-form-shipping-php\/"},"modified":"2026-09-22T11:25:59","modified_gmt":"2026-09-22T11:25:59","slug":"infinite-template-recursion-in-form-billing-php-form-shipping-php","status":"publish","type":"topic","link":"https:\/\/wordpress.org\/support\/topic\/infinite-template-recursion-in-form-billing-php-form-shipping-php\/","title":{"rendered":"Infinite template recursion in form-billing.php \/ form-shipping.php"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">ENVIRONMENT<\/p>\n\n\n\n<ul>\n<li>WordPress: 7.1.1<\/li>\n\n\n\n<li>WooCommerce: 11.1.1<\/li>\n\n\n\n<li>Fluid Checkout (free): 4.2.7<\/li>\n\n\n\n<li>PHP: 8.3 and 8.2 (reproduced on both)<\/li>\n\n\n\n<li>Server: LiteSpeed (Hostinger shared)<\/li>\n\n\n\n<li>Theme: Hello Theme by Elementor<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">SYMPTOM<br \/>Since updating to WooCommerce 11, the checkout page shows an endless spinner. The AJAX request <code>?wc-ajax=update_order_review<\/code> never finishes: the PHP process is killed and LiteSpeed returns HTTP 503. <code>get_refreshed_fragments<\/code> and every other wc-ajax endpoint work normally (~0.3s).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ROOT CAUSE<br \/>Your templates <code>fc\/checkout-steps\/checkout\/form-billing.php<\/code> and <code>form-shipping.php<\/code> fire the core actions for compatibility:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>do_action( 'woocommerce_checkout_billing', $checkout );   \/\/ form-billing.php\ndo_action( 'woocommerce_checkout_shipping', $checkout );  \/\/ form-shipping.php, line 78<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">WooCommerce core still has <code>WC_Checkout::checkout_form_billing<\/code> \/ <code>checkout_form_shipping<\/code> hooked on those actions. Fluid Checkout is supposed to remove them, but with WooCommerce 11 the <code>remove_action()<\/code> no longer matches the registered callback, so the core method stays attached. The template fires the action \u2192 core method calls <code>wc_get_template('checkout\/form-billing.php')<\/code> \u2192 Fluid Checkout&#8217;s template override is loaded again \u2192 it fires the action again \u2192 infinite loop until the process is killed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I suspect WooCommerce 11 changed how those callbacks are registered (e.g. first-class callable \/ Closure instead of <code>array( $this, 'method' )<\/code>), which is why the old <code>remove_action( $hook, array( WC()-&gt;checkout(), 'checkout_form_billing' ) )<\/code> silently fails.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">EVIDENCE<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">1) PHP 8.3 fatal (zend.max_allowed_stack_size):<br \/>PHP Fatal error: Maximum call stack size of 8339456 bytes reached during compilation \u2026\/fluid-checkout\/templates\/fc\/checkout-steps\/checkout\/form-shipping.php on line 64<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">2) Backtrace logged from inside the template (each 6-frame block repeats until the process dies):<br \/>04 wc_get_template<br \/>05 WC_Checkout-&gt;checkout_form_billing<br \/>06 WP_Hook-&gt;apply_filters<br \/>07 WP_Hook-&gt;do_action<br \/>08 do_action<br \/>09 include<br \/>10 wc_get_template<br \/>11 WC_Checkout-&gt;checkout_form_billing<br \/>12 WP_Hook-&gt;apply_filters<br \/>\u2026<br \/>Identical pattern for <code>checkout_form_shipping<\/code> \/ form-shipping.php.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">3) Template include counter: <code>checkout\/form-billing.php<\/code> included 38,050 times in a single update_order_review request before the process was killed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">4) Every nested include logs the same warnings because core passes only <code>$checkout<\/code>, not your variables:<br \/>PHP Warning: Undefined variable $is_billing_same_as_shipping in \u2026\/form-billing.php on line 23<br \/>PHP Warning: Undefined variable $billing_same_as_shipping_fields in \u2026\/form-billing.php on line 37<br \/>PHP Warning: foreach() argument must be of type array|object, null given in \u2026\/form-billing.php on line 37<br \/>PHP Warning: Undefined variable $billing_only_fields in \u2026\/form-billing.php on line 49<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">HOW TO REPRODUCE<\/p>\n\n\n\n<ol>\n<li>Clean WordPress install, WooCommerce 11.1, Fluid Checkout (latest), PHP 8.2 or 8.3.<\/li>\n\n\n\n<li>Add any product to the cart and open the checkout page.<\/li>\n\n\n\n<li>Open DevTools \u2192 Network. Observe <code>?wc-ajax=update_order_review<\/code>: it never returns (503 on LiteSpeed, 500\/timeout elsewhere). On PHP 8.3 the &#8220;Maximum call stack size&#8221; fatal above appears in debug.log.<\/li>\n\n\n\n<li>To confirm the loop, drop this in wp-content\/mu-plugins\/ and reload the checkout \u2014 it logs the include depth of the template:<br \/><a href=\"https:\/\/pastebin.com\/RcfdiRE7\">https:\/\/pastebin.com\/RcfdiRE7<\/a><\/li>\n\n\n\n<li>Also verify the core callback is still attached after Fluid Checkout has run (it should not be): <br \/><a href=\"https:\/\/pastebin.com\/6nH7vtZP\">https:\/\/pastebin.com\/6nH7vtZP<\/a><br \/><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">On my install the core callback is still present on both hooks when the checkout renders.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WORKAROUND I AM USING<br \/>A mu-plugin that blocks re-entry of the two templates via the <code>wc_get_template<\/code> filter (returns an empty file when the same template is requested while already rendering). With this in place update_order_review completes in ~0.6s and the checkout works. It is a band-aid; the proper fix is to make the core callback removal work with WooCommerce 11 (match by method name across array callbacks and Closures, or remove the <code>do_action<\/code> from the templates and re-add only third-party callbacks).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"template":"","class_list":["post-19026187","topic","type-topic","status-publish","hentry"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/19026187","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic"}],"about":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/types\/topic"}],"version-history":[{"count":0,"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/19026187\/revisions"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/media?parent=19026187"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}