A Cloudflare outage on June 13 took down wp-admin on a client site that does not use Cloudflare. The cause was a Yoast license check that received a 404-shaped response, treated it as valid, and cached it. Below is the stack trace, the line that broke, and the PR.
The site had zero DNS records at Cloudflare. The dependency arrived through the plugin.
The error
Uncaught Error: array_map(): Argument #2 ($array) must be of type array, null givenStack trace landed in wordpress-seo/inc/class-addon-manager.php on line 820. No dashboard, no plugins page, so no way to switch off the plugin from the screen the plugin had just taken away. 🙃
Disabling Yoast cleared it instantly.
Why a site not on Cloudflare went down
The site had 0 DNS records pointing at Cloudflare. Yoast does.
Yoast Premium validates its license against https://my.yoast.com/api/sites/current, and it makes that call on admin_init. That endpoint sits behind Cloudflare. So the site never touched Cloudflare and still inherited the outage, through a plugin, on every admin page load. ☁️
What the API returned
{ "statusCode": 404, "error": "Not Found", "message": "No sites found" }The plugin treated it as a valid response, then cached it. It asked a question, got a 404, wrote the 404 down and committed to it. One poisoned response broke every Yoast-enabled site on the machine until that cache cleared. ☠️
Where it blew up
'subscriptions' => array_map(
[ $this, 'map_subscription' ],
$site_information->subscriptions
),$site_information->subscriptions was null. Since PHP 8.0 that is a TypeError and not a warning, so it stops being a log line and starts being a white screen. 💥
Before
- Response used without any checks
- array_map() runs over null subscriptions
- PHP 8 raises a TypeError
- Every admin page load fatals
After the fix
- isset() on url and subscriptions
- is_array() check on subscriptions
- Falls back to default site information
- Bad response still cached
The strpos( $response_code, '200' ) check is also still in the same function.
The status code check
Same file, while reading around:
if ( $response_code === 200 || strpos( $response_code, '200' ) !== false ) {Cloudflare runs its own status codes in the 1200 range. 1200 contains 200, so it passes. Somewhere a Cloudflare error is being read as a valid license check and nobody in the transaction is aware of it. 🤡
Not the root cause here, but it is in the same function.
The fix
PR: https://github.com/Yoast/wordpress-seo/pull/22353
$response = $api_request->get_response();
if ( isset( $response->url, $response->subscriptions ) && is_array( $response->subscriptions ) ) {
return $response;
}
return $this->get_site_information_default();- checks
urlandsubscriptionsexist - checks
subscriptionsis actually an array - falls back to defaults when the response is incomplete
Four lines of isset() against one afternoon of no wp-admin.
What this does not cover
The cache. A bad response is still stored, it just no longer takes wp-admin down with it. The strpos( $response_code, '200' ) check is also still there.
Worth repeating
- a license check should never be able to fatal a CMS
array_map()is not validation- an external API is going to hand you something you did not plan for, and a plugin running on
admin_inithas to survive it
Cloudflare going down is not preventable. A plugin taking wp-admin down with it is. 🧯
