DEV Community

Axis Web Art
Axis Web Art

Posted on

A race condition in WordPress's page lifecycle was injecting "404" into my login page

I run a white label web agency. Every client site ends with the same setup: custom login page, branded emails, WordPress fingerprint removed. After doing it manually on dozens of sites I built a plugin to automate it. Then a client sent me a screenshot of a "404" error showing in a red box on the login page on a page that was loading fine.

This is what I found.

The setup

The plugin moves /wp-login.php to a custom slug — say /client-portal/. Direct requests to the old URL redirect home. The custom URL serves the real login page via template_redirect.

That part worked fine. But on the live site with Rank Math SEO installed, a "404" string was appearing in the login form's error output on every fresh page load — before any login attempt was made.

What was actually happening

WordPress runs its main query before template_redirect fires. The execution order inside WP::main() is:

  1. init hooks
  2. parse_request()
  3. query_posts() ← the main query runs here
  4. handle_404() ← 404 detection here
  5. wp action
  6. template_redirect ← where we serve the login page

For the custom slug /client-portal/, the main query at step 3 finds no matching post or page. So it sets $wp_query->is_404 = true. By the time we intercept the request in step 6 and serve wp-login.php, that flag has been set for three steps.

Rank Math hooks into WordPress's login_errors filter and checks is_404(). If it returns true, Rank Math injects "404" as an error string. WordPress passes it straight through to the login form output— no authentication attempt needed.

The fix

WordPress has a pre_handle_404 filter that fires inside handle_404() at step 4. Returning true from it prevents the 404 status header from being sent. But — and this is the part that took me a while — it does not reset $wp_query->is_404. That property is set in query_posts() at step 3, and nothing in handle_404() clears it. The filter only stops the header.

You have to reset it explicitly:

php
add_filter( 'pre_handle_404', [ $this, 'prevent_404' ], 1 );

public function prevent_404( bool $handled ): bool {
    global $wp_query;
    if ( $this->get_request_path() === $this->get_slug() ) {
        if ( $wp_query instanceof WP_Query ) {
            $wp_query->is_404 = false; // must be cleared explicitly
        }
        return true; // prevents 404 status header
    }
    return $handled;
}

Enter fullscreen mode Exit fullscreen mode

After this, is_404() returns false when login_errors fires. Rank Math
finds nothing to inject. The error box disappears.

The instanceof WP_Query guard matters — on certain admin and AJAX requests the global may not be initialised, and a typed parameter hint on the method would cause a fatal in those cases.

The interim-login popup problem WordPress has a heartbeat-based session check that shows a small popup when your admin session expires. It loads the login page in an iframe at roughly 400×500px and adds body.interim-login to the page body.

Every template in the plugin used full-page layout assumptions —
min-height: 100vh, flexbox stretch, fixed-position side panels. In a 400px iframe the username field was above the visible area with no scroll. Unusable.

The CSS fix scopes overrides to body.interim-login:

body.interim-login.als-login {
    display: block !important;
    min-height: 0 !important;
    background: var(--als-form-bg) !important;
}

/* Hide fixed panels that only make sense full-page */
body.interim-login.als-login.template-split::before,
body.interim-login.als-login.template-fullscreen::before {
    display: none !important;
}
Enter fullscreen mode Exit fullscreen mode

For the support footer that was overflowing the popup bottom, CSS wasn't reliable enough whether body.interim-login is added varies slightly between WordPress versions. A server-side check is unconditional:

public function footer_content(): void {
    if ( ! empty( $_REQUEST['interim-login'] ) ) return;
    // render footer...
}
Enter fullscreen mode Exit fullscreen mode

What I learned

The pre_handle_404 filter is documented as "fires before WordPress handles the 404" — which is accurate but incomplete. It fires before the response is handled. The query result that triggered the 404 detection is already set and stays set unless you clear it yourself.

If you're serving custom pages from template_redirect on URLs that don't match real posts, check whether any installed plugins read is_404() downstream. The pre_handle_404 filter is the right hook — just remember to reset the property, not only return true.

The plugin is free and open source if you want to look at the full
implementation: GitHub
· WordPress.org

I do white label WordPress development for agencies and this came out of real client work, most of the interesting bugs do.

Written with AI assistance #ABotWroteThis

Top comments (0)