Skip to content

fix(current): guard get_query_var when $wp_query is null (template-switching 500)#1127

Merged
superdav42 merged 1 commit intomainfrom
feature/auto-20260505-215453
May 6, 2026
Merged

fix(current): guard get_query_var when $wp_query is null (template-switching 500)#1127
superdav42 merged 1 commit intomainfrom
feature/auto-20260505-215453

Conversation

@superdav42
Copy link
Copy Markdown
Collaborator

@superdav42 superdav42 commented May 6, 2026

Summary

Fix a PHP 8+ fatal in Current::load_currents() that surfaces in production
as "A network error occurred. Please check your connection and try again."
on the customer-panel Switch Template page (and any other wu-ajax
endpoint that reads is_customer_allowed()).

Root cause

PR #1118 added a lazy-load entry from Current::get_customer() /
get_membership() so callers that arrive before the init/wp cache
warm-up still get a populated value. That fix is correct in spirit but
exposes a latent hazard inside load_currents() itself: both pretty-URL
hash overrides call get_query_var().

get_query_var() is $wp_query->get($var, $default). The wu-ajax
pipeline (Light_Ajax::process_light_ajax) dispatches wu_ajax_*
handlers at plugins_loaded priority 20 — long before WordPress reaches
parse_query. At that point $wp_query is null, and PHP 8+ fatals
with Call to a member function get() on null. The AJAX handler dies
with HTTP 500 and no JSON body, so the JS error callback shows the
generic network-error banner.

Backtrace from a real customer (production):

#0 wp-includes/query.php:29  — get_query_var('site_hash')
#1 inc/class-current.php:253 — load_currents()
#2 inc/class-current.php:377 — get_customer()
#3 inc/models/class-site.php:1075 — Site->is_customer_allowed()
#4 inc/ui/class-template-switching-element.php:320 — switch_template()
...
#7 inc/class-light-ajax.php:160 — do_action('wu_ajax_wu_switch_template')
#11 wp-settings.php:593 — do_action('plugins_loaded')

Fix

Compute $query_vars_ready = did_action('parse_query') && $GLOBALS['wp_query'] instanceof WP_Query
once at the top of load_currents(). When false, substitute an empty
string for both get_query_var() calls. wu_request() still picks up
$_REQUEST overrides if present, and the wu-ajax pipeline carries no
rewrite-driven site_hash / membership_hash anyway (it is a flat
?wu-ajax=1 POST), so nothing functional is lost. Once WP reaches wp
and re-runs load_currents(), the guard is true and the original
behaviour is preserved verbatim.

Test plan

  • New regression test test_load_currents_survives_when_wp_query_is_null
    nulls $GLOBALS['wp_query'] and asserts load_currents() runs to
    completion. Verified by reverting the fix and confirming the test
    reproduces the production fatal exactly:

    Error: Call to a member function get() on null at class-current.php:253

  • Full Current_Test suite: 11 tests, 16 assertions, all pass.
  • PHPCS: clean.
  • PHPStan: clean.
  • Other test failures in the suite are pre-existing and unrelated
    (verified by running the same scope on origin/main).

Files changed

  • inc/class-current.php — guard both get_query_var() calls.
  • tests/WP_Ultimo/Current_Test.php — regression test.

Resolves the customer-reported 500 / "A network error occurred" banner
on the customer-panel template-switching page reported after #1118
landed.


aidevops.sh v3.14.75 plugin for OpenCode v1.14.33 with claude-sonnet-4-6 spent 1d 23h on this as a headless worker.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed potential errors that could occur during specific WordPress initialization scenarios, improving system stability.
  • Performance

    • Optimized object loading to prevent unnecessary reprocessing in administrative and background request contexts.

The lazy-load entry path added in PR #1118 made `Current::get_customer()`
call `Current::load_currents()` whenever the cache is null. That worked
for normal init/wp pipeline calls, but broke the wu-ajax pipeline:

`Light_Ajax::process_light_ajax` dispatches `wu_ajax_*` handlers at
`plugins_loaded` priority 20 and `die()`s before WordPress core gets
to `parse_query`. The handler for `wu_switch_template` reads
`Site::is_customer_allowed()`, which calls `get_customer()`, which
(after #1118) calls `load_currents()` on the still-cold cache. Inside
`load_currents()` the frontend branch reads `get_query_var('site_hash')`
and the membership branch reads `get_query_var('membership_hash')`.

`get_query_var()` is `$wp_query->get($var, $default)`, and at
`plugins_loaded` `$wp_query` is still null. PHP 8+ fatals with
`Call to a member function get() on null`, the AJAX handler returns
HTTP 500 with no JSON body, and the customer-panel template-switching
UI surfaces the generic "A network error occurred. Please check your
connection and try again." banner. A real customer hit this on
production immediately after #1118 landed; backtrace points squarely
at class-current.php line 253.

Guard both `get_query_var()` reads with a `$query_vars_ready` check
that confirms `parse_query` has fired and `$GLOBALS['wp_query']` is a
real `WP_Query` instance. When the query var system is not ready,
substitute an empty-string default — `wu_request()` will still pick
up `$_REQUEST` overrides if present, and the wu-ajax pipeline never
carries pretty-URL `site_hash` / `membership_hash` query vars anyway
(it is a flat `?wu-ajax=1` POST), so nothing functional is lost. Once
WordPress reaches `wp` and reruns `load_currents()`, the guard is
true and the original behaviour is preserved verbatim.

Add a regression test that nulls `$GLOBALS['wp_query']` to simulate
the early-call condition and asserts `load_currents()` completes
without fataling. Verified by reverting the fix and confirming the
test reproduces the production fatal exactly.

PHPCS clean. PHPStan clean. All Current_Test cases pass (11 / 11,
16 assertions).

Resolves the customer-reported "A network error occurred" / 500 on
the customer-panel template-switching page.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e32b6aa1-9bad-4a56-a905-68cf0a3500cb

📥 Commits

Reviewing files that changed from the base of the PR and between 217b5dd and 6bc48d5.

📒 Files selected for processing (2)
  • inc/class-current.php
  • tests/WP_Ultimo/Current_Test.php

📝 Walkthrough

Walkthrough

This PR adds defensive logic to load_currents() to prevent fatal errors when called before WordPress parses query variables. It introduces a $query_vars_ready flag and replaces direct get_query_var() calls with guarded defaults, safely handling cases where $wp_query is null, plus a regression test validating the fix.

Changes

Query Variable Safety Hardening

Layer / File(s) Summary
Early Exit Optimization
inc/class-current.php
Add early return in load_currents() when init has run, site is set, and request is admin or AJAX, preventing unnecessary reloading.
Query Readiness Detection
inc/class-current.php
Introduce $query_vars_ready flag based on did_action('parse_query') and presence of global WP_Query to gate safe access to query variables.
Guarded Query Variable Access
inc/class-current.php
Replace direct get_query_var() calls for site_hash and membership_hash with default variables computed only when $query_vars_ready is true, preventing PHP errors during early WordPress lifecycle phases.
Regression Test
tests/WP_Ultimo/Current_Test.php
Add test_load_currents_survives_when_wp_query_is_null() to validate load_currents() completes without fatal errors when called with null $wp_query.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through WordPress's dance,
Catching query vars before they're in trance.
With guards and flags, we leap with care—
No null WP_Query catches us mid-air! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the specific issue being fixed: guarding get_query_var when $wp_query is null to resolve a template-switching 500 error, directly corresponding to the main change in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/auto-20260505-215453

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Performance Test Results

Performance test results for fdd0e44 are in 🛎️!

Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown.

URL: /

Run DB Queries Memory Before Template Template WP Total LCP TTFB LCP - TTFB
0 40 37.78 MB 804.50 ms 156.50 ms (-4.00 ms / -3% ) 1060.00 ms (+70.50 ms / +7% ) 2022.00 ms 1912.95 ms 86.60 ms
1 56 49.11 MB 940.00 ms 141.50 ms 1079.50 ms 2082.00 ms 2001.00 ms 79.20 ms

@superdav42 superdav42 merged commit d804737 into main May 6, 2026
11 checks passed
@superdav42
Copy link
Copy Markdown
Collaborator Author

Summary

Fix a PHP 8+ fatal in Current::load_currents() that surfaces in production
as "A network error occurred. Please check your connection and try again."
on the customer-panel Switch Template page (and any other wu-ajax
endpoint that reads is_customer_allowed()).

Root cause

PR #1118 added a lazy-load entry from Current::get_customer() /
get_membership() so callers that arrive before the init/wp cache
warm-up still get a populated value. That fix is correct in spirit but
exposes a latent hazard inside load_currents() itself: both pretty-URL
hash overrides call get_query_var().
get_query_var() is $wp_query->get($var, $default). The wu-ajax
pipeline (Light_Ajax::process_light_ajax) dispatches wu_ajax_*
handlers at plugins_loaded priority 20 — long before WordPress reaches
parse_query. At that point $wp_query is null, and PHP 8+ fatals
with Call to a member function get() on null. The AJAX handler dies
with HTTP 500 and no JSON body, so the JS error callback shows the
generic network-error banner.
Backtrace from a real customer (production):

#0 wp-includes/query.php:29  — get_query_var('site_hash')
#1 inc/class-current.php:253 — load_currents()
#2 inc/class-current.php:377 — get_customer()
#3 inc/models/class-site.php:1075 — Site->is_customer_allowed()
#4 inc/ui/class-template-switching-element.php:320 — switch_template()
...
#7 inc/class-light-ajax.php:160 — do_action('wu_ajax_wu_switch_template')
#11 wp-settings.php:593 — do_action('plugins_loaded')

Fix

Compute $query_vars_ready = did_action('parse_query') && $GLOBALS['wp_query'] instanceof WP_Query
once at the top of load_currents(). When false, substitute an empty
string for both get_query_var() calls. wu_request() still picks up
$_REQUEST overrides if present, and the wu-ajax pipeline carries no
rewrite-driven site_hash / membership_hash anyway (it is a flat
?wu-ajax=1 POST), so nothing functional is lost. Once WP reaches wp
and re-runs load_currents(), the guard is true and the original
behaviour is preserved verbatim.

Test plan

  • New regression test test_load_currents_survives_when_wp_query_is_null
    nulls $GLOBALS['wp_query'] and asserts load_currents() runs to
    completion. Verified by reverting the fix and confirming the test
    reproduces the production fatal exactly:

    Error: Call to a member function get() on null at class-current.php:253

  • Full Current_Test suite: 11 tests, 16 assertions, all pass.
  • PHPCS: clean.
  • PHPStan: clean.
  • Other test failures in the suite are pre-existing and unrelated
    (verified by running the same scope on origin/main).

Files changed

  • inc/class-current.php — guard both get_query_var() calls.
  • tests/WP_Ultimo/Current_Test.php — regression test.
    Resolves the customer-reported 500 / "A network error occurred" banner
    on the customer-panel template-switching page reported after GH#1114: fix(current): lazy-load currents before init #1118
    landed.

aidevops.sh v3.14.75 plugin for OpenCode v1.14.33 with claude-sonnet-4-6 spent 1d 23h on this as a headless worker.


Merged via PR #1127 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant