If you’ve ever written a redirect in WordPress, you know it’s usually simple: check a condition, call wp_safe_redirect(), and you’re done.
That’s why it’s confusing when a redirect works perfectly one moment and fails the next.
I recently ran into that exact situation. My redirect logic looked correct, the conditions were clear, yet users were being sent to the wrong page or caught in a loop.
Even stranger, everything worked fine in Firefox but not in Chrome.
What began as a small template_redirect snippet turned into two days of debugging, and the cause wasn’t in my code at all. It was in how modern browsers “help” load pages faster through prefetching.
In this post, I’ll walk you through what happened, what I learned about speculative requests, and how a single if check fixed the problem.
The Setup: The Gating Logic
The original goal was simple — protect the checkout page so only logged-in users could access it.
Guests trying to open /checkout should be redirected to the cart page, where a login modal appears.
After logging in, they’re automatically sent back to the checkout.
Here’s the logic in code:
add_action('template_redirect', function () {
// 1) If user is logged in and we have the gate flag → go straight to Checkout.
if (is_user_logged_in() && isset($_GET['login_required'])) {
wp_safe_redirect(wc_get_checkout_url());
exit;
}
// 2) Guard Checkout: guests can’t open /checkout (non-AJAX, non-REST).
if (
! is_user_logged_in()
&& is_checkout()
&& ! wp_doing_ajax()
&& ! defined('REST_REQUEST')
) {
wp_safe_redirect(add_query_arg('login_required', '1', wc_get_cart_url()));
exit;
}
}, 0);It looks straightforward.
And in most cases, it worked perfectly — the redirect did exactly what it should.
But only most cases.
In some browsers, users logged in successfully but still got pushed back to the cart page, as if the login didn’t happen at all.
The Bug: The Inconsistent Redirect
Everything looked right on paper. The redirect logic was solid, and normal logins worked exactly as expected.
The issue appeared only when users logged in via AJAX-based methods — things like social login, OTP login, or any popup login form that didn’t reload the page.
After logging in, the popup confirmed success, but instead of landing on the checkout page, users were bounced back to the cart again.
Even stranger, this happened only in Chrome.
In Firefox, the same flow worked flawlessly.
That inconsistency was the first real clue. If the PHP logic didn’t change, but browser choice did, the problem had to be happening before the code even saw the request.
It was time to start digging deeper.
The Investigation: Digging Into the Requests
After hours of testing different login methods, I decided to stop guessing and start logging — the simplest step often reveals the most.
I added this small snippet at the very top of my template_redirect callback:
if (is_checkout()) {
error_log(print_r([
is_user_logged_in(),
$_SERVER
], true));
}This log instantly showed something unexpected.
The hook wasn’t firing once per request—it was firing twice for the same /checkout URL.
On the first hit, is_user_logged_in() returned false.
On the second, just a moment later, it returned true.
The logs confirmed it. Both requests hit the same path, but there were two key differences:
- The first request had no login cookie.
- The first request contained a header:
HTTP_SEC_PURPOSE: prefetch
That was the turning point.
The “ghost” request wasn’t coming from WordPress or WooCommerce — it was coming from the browser itself.
The Cause: Browser Prefetch Behavior

At first it felt like a WordPress problem, but it wasn’t.
It was the browser.
Modern browsers like Chrome, Edge, and sometimes Safari use prefetch and prerender to speed up navigation. They request pages before the user actually clicks. These speculative requests often arrive without cookies or session data for privacy and security reasons.
Here’s what happens in practice:
- The first request looks like a normal page visit but includes a header such as
HTTP_SEC_PURPOSE: prefetch. It has nowordpress_logged_incookie. - WordPress sees a guest and runs your redirect logic.
- A moment later, the real navigation follows with full cookies and the correct login state, but your redirect has already fired.
Detecting and ignoring these speculative requests fixes the issue. You stop acting on visits that aren’t real user navigations.
The Fix: Ignore Speculative Loads
The safest approach is to bail out early when the request is a prefetch/prerender.
Check for the relevant headers, and skip any redirect logic for those requests.
add_action('template_redirect', function () {
// 0) Ignore speculative requests (prefetch / prerender).
$is_prefetch =
(!empty($_SERVER['HTTP_SEC_PURPOSE']) && stripos($_SERVER['HTTP_SEC_PURPOSE'], 'prefetch') !== false) ||
(!empty($_SERVER['HTTP_PURPOSE']) && stripos($_SERVER['HTTP_PURPOSE'], 'prefetch') !== false);
if ($is_prefetch) {
return; // Do nothing for prefetch. Treat only real navigations.
}
// 1) If user is logged in and we have the gate flag → go straight to Checkout.
if (is_user_logged_in() && isset($_GET['login_required'])) {
wp_safe_redirect(wc_get_checkout_url());
exit;
}
// 2) Guard Checkout: guests can’t open /checkout (non-AJAX, non-REST).
if (
! is_user_logged_in()
&& function_exists('is_checkout') && is_checkout()
&& ! wp_doing_ajax()
&& ! defined('REST_REQUEST')
) {
wp_safe_redirect(add_query_arg('login_required', '1', wc_get_cart_url()));
exit;
}
}, 0);This small check prevents WordPress from acting on a request that isn’t a real visit.
Your actual navigation arrives next, with cookies, and the redirect behaves correctly.
Lessons Learned
This issue was small but revealing. It showed how something that looks like a WordPress bug can actually come from the browser.
Here are the main takeaways:
- Browsers can send speculative requests like prefetch or prerender before the user even clicks a link.
- Those requests often arrive without cookies or session data, which makes logged-in checks unreliable.
- WordPress runs
template_redirecton every request, even speculative ones. - Ignoring prefetch requests prevents false redirects and protects flows like logins, checkouts, or gated content.
- Always log
$_SERVERwhen behavior feels inconsistent—it often tells you more than any print_r of your variables.
The fix was a single line, but the lesson was bigger: sometimes the issue isn’t in your PHP — it’s in what the browser thinks it’s helping you with.