{"banners":{"high":"https://res.cloudinary.com/demo/image/upload/c_fill,w_1544,h_500/sample.jpg","low":"https://res.cloudinary.com/demo/image/upload/c_fill,w_722,h_250/sample.jpg"},"requires_php":"7.0","version":"1.5.7","last_updated":"2026-10-18 18:51:50","slug":"404-litespeed-fallback","sections":{"installation":"Click the activate button to install.","changelog":"<h4>1.5.7 &mdash; 18 October 2026</h4>\n\t\t<ul>\n\t\t  <li>Update offers now appear even when the site cannot reach WordPress.org.</li>\n\t\t</ul>\n\n\t\t<h4>1.5.6 &mdash; 18 October 2026</h4>\n\t\t<ul>\n\t\t  <li>Purge-on-404 now honours the JS/CSS fallback toggles. The missing-bundle handler used to mark the response uncacheable and run the configured purges (including LiteSpeed <em>Purge All</em>) before checking the toggles, so with both fallbacks off any request for a missing <code>/litespeed/</code> bundle still purged the whole cache and then returned a normal 404. The marking and purges now run only when a fallback is actually served for that asset type, matching the inline catcher and the 1.5.1 design (server-side purge happens via the 307 route). With the relevant toggle on, behaviour is unchanged. The CSS-disabled 410 stays uncacheable but no longer purges.</li>\n\t\t  <li><code>wp ls404fb health</code> no longer reports the inline catcher as OK when LiteSpeed Cache is inactive: it now shows <em>idle (LiteSpeed Cache inactive)</em> as INFO, leaving the existing LiteSpeed Cache ERROR row to set the exit code.</li>\n\t\t</ul>\n\n\t\t<h4>1.5.5 &mdash; 18 October 2026</h4>\n\t\t<ul>\n\t\t  <li>Background auto-updates now work (WP-cron, WP-CLI and Easy Updates Manager), plus updater robustness fixes.</li>\n\t\t</ul>\n\n\t\t<h4>1.5.4 &mdash; 18 September 2026</h4>\n\t\t<ul>\n\t\t  <li>The missing-bundle handler now runs at <code>template_redirect</code> priority 0 instead of the default 10. Multilingual plugins (TranslatePress, WPML, Polylang) hook the same action at 10 but from an earlier <code>init</code>, so they were answering first: a missing bundle is not a file on disk, so they treated the URL as a page and 301&rsquo;d it to the language-prefixed form, costing a second full WordPress boot before the fallback could respond. On one measured site that was 3.4&nbsp;s for the stylesheet route and ~4.8&nbsp;s of &ldquo;Loading&hellip;&rdquo; on screen before the reloader fired &mdash; the visitor-visible grey-screen-then-redirect. The handler now answers on the first boot; the route is one boot plus a static file.</li>\n\t\t  <li>The 307 (and the CSS-disabled 410) are now explicitly uncacheable. They were leaving with the page-cache plugins&rsquo; normal headers (<code>s-maxage=345600</code> from Super Page Cache; <code>public,max-age=3600</code> tagged <code>HTTP.404</code> from LiteSpeed Cache), so a cached redirect became the edge&rsquo;s answer for that bundle URL even after the bundle was regenerated. The handler now sends <code>Cache-Control: no-store, no-cache, must-revalidate, max-age=0</code> and <code>X-LiteSpeed-Cache-Control: no-cache</code>, calls <code>litespeed_control_set_nocache</code>, and re-asserts both headers at <code>send_headers</code> / <code>wp_headers</code> priority <code>PHP_INT_MAX</code>, and &mdash; the part that actually closes the window &mdash; now <code>exit</code>s after sending the redirect. Previously it fell through to WordPress&rsquo;s normal shutdown, which is where a later callback could replace the <code>Location</code> and where the page-cache plugins stamped their headers. Verified on the wire against a TranslatePress-style redirect using WordPress&rsquo;s real hook engine: 1.5.3 left with <code>s-maxage=345600</code>, 1.5.4 leaves with <code>no-store</code>. LiteSpeed Cache&rsquo;s <code>X-LiteSpeed-Tag: HTTP.404</code> is set before the handler runs and still appears; it is inert once <code>X-LiteSpeed-Cache-Control: no-cache</code> is present.</li>\n\t\t  <li>Documented which healing path actually fires: where the <code>.htaccess</code> routing block is present every hop returns 200, so the inline catcher&rsquo;s resource-error listener never fires and the 307 route is the whole story; the catcher only acts where a bundle 404s outright at the server. Both reloader stubs now publish <code>window.ls404fb</code> (asset, route, start time) as their first statement, so timing and available fields are identical whichever route delivered them.</li>\n\t\t  <li>Confirmed, no change needed: since 1.5.3 the Cloudflare-bypass check reads only <code>cf_fallback_cache_excluded_urls</code> on Super Page Cache &ge;&nbsp;5.1 and only <code>cf_excluded_urls</code> on &le;&nbsp;5.0.</li>\n\t\t</ul>\n\n\t\t<h4>1.5.3 &mdash; 17 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Cloudflare-bypass detection is now version-aware. Super Page Cache&rsquo;s rule builder reads <code>Constants::SETTING_EXCLUDED_URLS</code>, and that constant changed value in 5.1.0: on &le;&nbsp;5.0.12 it is <code>cf_excluded_urls</code> (a separate <code>cf_fallback_cache_excluded_urls</code> exists there but only governs the local disk cache), on &ge;&nbsp;5.1.0 the two lists were merged into <code>cf_fallback_cache_excluded_urls</code>. 1.4.4 read the latter unconditionally, which on &le;&nbsp;5.0 reported &ldquo;ok&rdquo; when only the disk-cache list held <code>*LSCWP_CTRL=NOCACHE*</code> while Cloudflare still cached the reload. The health check, the two notices and the settings deep link now all key off the same version test and can no longer disagree. The blob-wide sweep and guessed option names from earlier releases are removed, since on old SPC they re-create the false positive.</li>\n\t\t  <li>Heads-up from the investigation: SPC ships no migration for this rename, so a bypass configured on &le;&nbsp;5.0 silently stops being honoured after upgrading to 5.1+ until <code>*LSCWP_CTRL=NOCACHE*</code> is re-added in the new field. The notices will flag exactly that.</li>\n\t\t</ul>\n\n\t\t<h4>1.5.2 &mdash; 17 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Fixed the Cloudflare-bypass notices&rsquo; deep link on Super Page Cache 5.1 and later, where it produced WordPress&rsquo;s &ldquo;Sorry, you are not allowed to access this page.&rdquo; SPC 5.1.0 moved its settings from a <em>Settings</em> submenu (<code>wp-cloudflare-super-page-cache-index</code>) to a top-level menu (<code>super-page-cache-settings</code>) and reworded the exclude-list field from &ldquo;Prevent the following URIs to be cached&rdquo; to &ldquo;&hellip;from being cached&rdquo;. Both notices now ask WordPress which page is actually registered (falling back to the <code>SWCFPC_VERSION</code> constant, which first appeared in 5.1.0), link to the right one &mdash; with <code>#general</code> on 5.1+ so it lands on the tab holding the field &mdash; and quote the field label the admin will actually see. Verified against the wp.org source for 4.7.13 through 5.3.5. The legacy link is byte-identical to before.</li>\n\t\t  <li>The dashboard-wide notice now uses the same compact one-link sentence as the settings-page banner, and both drop the &ldquo;loop-guard&rdquo; jargon in favour of plain wording.</li>\n\t\t</ul>\n\n\t\t<h4>1.5.1 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Inline catcher is now a detector only. Instead of carrying its own copy of the reload logic, the inline trigger (&asymp;1.2&nbsp;KB, most of it the two absolute asset URLs) hot-loads the plugin&rsquo;s canonical reloader (<code>public/js/critical-fallback.js</code>, or <code>critical-fallback-force-styles.js</code> when CSS fallback + &ldquo;wait for CSS&rdquo; are on &mdash; the same variant selection the 307 route uses, now shared via <code>ls404fb_fallback_js_url()</code>). One reloader implementation, the force-styles behaviour is preserved, no reload logic lives in cached HTML, and reloader fixes take effect on the next incident without waiting for edge-cached HTML to expire. If the reloader itself fails to load, the trigger performs a bare loop-guarded cache-bypass reload so the page is never stranded.</li>\n\t\t  <li>Removed the beacon telemetry and the <code>/wp-json/ls404fb/v1/dead-bundle</code> REST endpoint introduced in 1.5.0. The plugin now sends nothing anywhere; server-side purge-on-catch happens only via the <code>worker.php</code> 307 route (best-effort on LiteSpeed Cache 7.9+).</li>\n\t\t  <li>Hardened both reloader stubs for running before <code>&lt;body&gt;</code> exists (the dead bundle tag in <code>&lt;head&gt;</code>): they now target <code>&lt;html&gt;</code> until <code>&lt;body&gt;</code> is parsed, and their last-resort fallback keeps the visitor on the same path + hash instead of bouncing to the homepage. This latent bug also affected the existing 307 path on sites that don&rsquo;t defer JS.</li>\n\t\t  <li>The force-styles stub no longer hard-codes a version for its CSS URL (it had been stuck at <code>?ver=1.1.7</code>): the trigger passes a versioned URL, and on the 307 path it derives the URL from the dead bundle&rsquo;s own <code>?ver=</code>. It also injects whatever CSS it gets rather than abandoning the reload on a failed fetch.</li>\n\t\t</ul>\n\n\t\t<h4>1.5.0 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>New primary healing mechanism: an <em>inline catcher</em>. LiteSpeed Cache 7.9+ seeds <code>wp-content/litespeed/.htaccess</code> with a <code>STATIC_PROTECT_V2</code> block that hard-404s missing bundles at the web-server layer, and rewrites that file (strict byte-equality) from <code>File::save()</code> on <em>every</em> write under the static folder with no hook &mdash; so any routing block the plugin adds is wiped within seconds on a busy site and the 307-to-stub path never fires. The catcher sidesteps this entirely: a ~700-byte <code>data-no-optimize</code> script injected at the top of <code>&lt;head&gt;</code> (via <code>litespeed_buffer_finalize</code>, with a <code>wp_head</code> fallback for Optimax snapshots) listens for resource-load errors on <code>/litespeed/(js|css)/</code> bundles and performs the same hide &rarr; <code>?LSCWP_CTRL=NOCACHE</code> &rarr; reload with the same loop-guard, needing no server routing at all. Honours the JS/CSS toggles in Basic Settings; default on; new toggle under <em>Self-Heal Settings</em>.</li>\n\t\t  <li>Added a hardened public beacon endpoint <code>POST /wp-json/ls404fb/v1/dead-bundle</code> that the catcher pings via <code>sendBeacon</code>, so a caught dead bundle still runs the configured LiteSpeed / Cloudflare purges for everyone (the second half of the original worker design). Only acts on <code>/litespeed/(css|js)/</code> paths that are genuinely absent on disk; path-sanitised; per-(IP, asset) throttle of 120&nbsp;s and a site-wide purge-storm guard of one purge per 60&nbsp;s.</li>\n\t\t  <li>Demoted the <code>.htaccess</code> self-heal to supplementary (kept for the Toolbox direct-URL <code>307</code> verification, non-JS clients and pre-7.9 LiteSpeed), and made it hold up better on 7.9+: a frontend heartbeat on <code>wp</code> and <code>shutdown</code> re-applies the block within the same request a wipe happened in, gated on a single <code>stat()</code> (mtime + size) so an unchanged file costs nothing; LSCWP hook priorities raised to <code>PHP_INT_MAX</code>; writes stay tmp+rename so they succeed even when the file is locked.</li>\n\t\t  <li>Defense in depth: a dashboard-wide <code>notice-warning</code> (and a matching <code>wp ls404fb health</code> row and settings-page pill state) now flags when the LiteSpeed <code>STATIC_PROTECT</code> seeding is present but <code>wp-content/litespeed/.htaccess</code> is still writable, advising <code>chmod 444</code>. Locked, LiteSpeed&rsquo;s <code>file_put_contents()</code> wipe fails while the plugin&rsquo;s rename-based update still works. The plugin advises only &mdash; it never changes permissions on the operator&rsquo;s behalf (a locked file also blocks LiteSpeed&rsquo;s own rare template bumps, which the health check surfaces).</li>\n\t\t  <li>Docs honesty: the Self-Heal Settings section now states plainly that both layers are safety nets, that the <code>.htaccess</code> route races an auto-generated file, and that the durable fixes are hosting-side (edge TTL below origin TTL; JS/CSS Combine off). Pages cached before 1.5.0 won&rsquo;t carry the catcher until they expire.</li>\n\t\t</ul>\n\n\t\t<h4>1.4.4 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Followed up on 1.4.3&rsquo;s Cloudflare-bypass detector fix &mdash; the truly correct SWCFPC key is <code>swcfpc_config['cf_fallback_cache_excluded_urls']</code> (the exclude list that actually feeds the Cloudflare page rule), not <code>cf_excluded_urls</code> (a different list SWCFPC keeps for other purposes). 1.4.3&rsquo;s false-positive therefore persisted on any site with <code>*LSCWP_CTRL=NOCACHE*</code> correctly excluded. Now checks the CF-feeding key first, then <code>cf_excluded_urls</code>, then the broader <code>swcfpc_config</code> sweep, then the legacy option keys.</li>\n\t\t</ul>\n\n\t\t<h4>1.4.3 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Fixed a false-positive in the Cloudflare bypass detector that flagged every SPC site as unconfigured regardless of truth. <code>ls404fb_verify_cloudflare_bypass()</code> was reading the wrong option keys (<code>swcfpc_cache_settings</code>, <code>swcfpc_settings</code>) &mdash; SWCFPC actually stores its Cloudflare exclude list at <code>swcfpc_config['cf_excluded_urls']</code>. Now reads the correct key first, then falls back to a broader sweep of <code>swcfpc_config</code> (for future key renames), then the legacy keys (for pre-<code>swcfpc_config</code> installs). This silences the dashboard-wide <em>notice-error</em> and the CLI health <em>ERROR</em> on sites that already had <code>*LSCWP_CTRL=NOCACHE*</code> correctly excluded.</li>\n\t\t</ul>\n\n\t\t<h4>1.4.2 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Cloudflare Super Cache bypass-missing warning now shows as a dashboard-wide <code>notice-error</code> across every admin page (except the plugin&rsquo;s own Settings page, where the inline banner already surfaces it), so admins can&rsquo;t miss the config gap that silently defeats the reload trick. Non-dismissible &mdash; the notice only clears when <code>*LSCWP_CTRL=NOCACHE*</code> is actually added to the SPCFC bypass list. Restricted to users with <code>manage_options</code>.</li>\n\t\t</ul>\n\n\t\t<h4>1.4.1 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Replaced the naive <code>DISABLE_WP_CRON</code> warning with a real liveness check. Every self-heal cron pulse now stamps <code>ls404fb_selfheal_last_check</code>; the health report compares that timestamp against the 15-minute interval and emits <em>OK</em> within 2&times; the interval, <em>WARNING</em> within 4&times;, <em>ERROR</em> beyond that &mdash; regardless of whether WP-Cron is native or a server-side ticker is hitting <code>wp-cron.php</code>. Added overdue detection on the queued run time too: if <code>wp_next_scheduled</code> returns a timestamp significantly in the past, the event is queued but nothing is dispatching it. <code>DISABLE_WP_CRON</code> is now an <em>INFO</em> config note, not a warning &mdash; it&rsquo;s the correct setup on many fleets.</li>\n\t\t</ul>\n\n\t\t<h4>1.4.0 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Added a WP-CLI command surface: <code>wp ls404fb init</code> bootstraps the plugin state (schedules the self-heal cron, applies the <code>.htaccess</code> patch, migrates the legacy purge-option key), <code>wp ls404fb health</code> prints a table of every integration check (LiteSpeed Cache active + version, Cloudflare Super Cache + <code>LSCWP_CTRL=NOCACHE</code> bypass, self-heal toggle, target file writability, marker block state, drift, last-applied time, next cron, WP-Cron enabled, purge-option migration state, purge-on-404 wiring, fallback toggles, debug webhook, update endpoint reachability) with pass/warn/error rows and a matching exit code, and <code>wp ls404fb apply</code> force-re-applies the <code>.htaccess</code> patch on demand. Supports <code>--format=json|csv|yaml</code> and <code>--strict</code>.</li>\n\t\t</ul>\n\n\t\t<h4>1.3.1 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Ensured the 15-minute self-heal cron is (re-)scheduled on settings save, on the Plugins screen, on the WordPress Dashboard, and on this plugin&rsquo;s own Settings page &mdash; so in-place upgrades from 1.2.x that don&rsquo;t re-run activation still pick up the cron. Toggling the Self-Heal switch now also unschedules the cron immediately when turned off. All calls are idempotent.</li>\n\t\t</ul>\n\n\t\t<h4>1.3.0 &mdash; 4 September 2026</h4>\n\t\t<ul>\n\t\t  <li>Resurrected the 404 reload trick against newer LiteSpeed Cache releases, whose auto-generated <code>wp-content/litespeed/.htaccess</code> (marker <code>LSCACHE_STATIC_PROTECT_V2</code>) short-circuits missing bundle requests at the web-server layer before WordPress can see them. The plugin now prepends its own marker block above LiteSpeed&rsquo;s that routes missing <code>js</code>/<code>css</code>/<code>ucss</code>/<code>ccss</code> files back to <code>index.php</code>, and re-applies the patch on <code>litespeed_purged_all</code>, <code>litespeed_purged_all_cssjs</code>, <code>litespeed_update_confs</code>, plugin upgrades, admin page loads (mtime-guarded), and a 15-minute cron backstop.</li>\n\t\t  <li>Added a new <em>Self-Heal Settings</em> section with a default-on toggle and a live status pill (Patched &amp; Watching / Waiting for LiteSpeed / Patch Missing / File Not Writable / Self-Heal Disabled) plus a hover tooltip showing the target file, writability, marker state, drift flag, last-applied timestamp, and next cron sweep.</li>\n\t\t  <li>Fixed a long-standing typo in the purge-on-404 handler (<code>ls404fb_puging_settings</code> &rarr; <code>ls404fb_purging_settings</code>) that silently ignored the Purging Settings toggles since the plugin&rsquo;s original release. Adds a one-time migration so existing installs don&rsquo;t need to re-save.</li>\n\t\t  <li>Replaced the generic &ldquo;check your Cloudflare Super Cache config&rdquo; nag with an active health check that reads SPCFC&rsquo;s stored options and shows a green success banner when <code>LSCWP_CTRL=NOCACHE</code> is on the bypass list, or a red error banner when it isn&rsquo;t.</li>\n\t\t</ul>\n\n\t\t<h4>1.2.5 &mdash; 6 June 2026</h4>\n\t\t<ul>\n\t\t  <li>Slimmed the guard status tooltip to just the trigger conditions (LiteSpeed Cache + CSS Combine / Load CSS Async / Generate UCSS), and added an info icon to the chip to signal the tooltip.</li>\n\t\t</ul>\n\n\t\t<h4>1.2.4 &mdash; 6 June 2026</h4>\n\t\t<ul>\n\t\t  <li>Guard status chip now uses the same <code>&lt;code&gt;</code> styling and spacing as the SPC Purge / LiteSpeed Purge status chips, with added amber (needed but off) and grey (not needed) states.</li>\n\t\t</ul>\n\n\t\t<h4>1.2.3 &mdash; 6 June 2026</h4>\n\t\t<ul>\n\t\t  <li>Streamlined the Repair CSS Combine status display to a single inline \"Guard Needed / Guard Not Needed\" pill (green = needed and on, red = needed but switched off, grey = not needed), with the full per-condition breakdown moved into a hover/focus tooltip. Presentation only &mdash; the runtime gate is unchanged.</li>\n\t\t</ul>\n\n\t\t<h4>1.2.2 &mdash; 6 June 2026</h4>\n\t\t<ul>\n\t\t  <li>Repair CSS Combine now self-disables at runtime when its preconditions aren't met (LiteSpeed active with CSS Combine, Load CSS Async, or Generate UCSS on) &mdash; skipping the per-tag and full-buffer work on sites that don't need it.</li>\n\t\t  <li>Added a live status panel under the toggle showing each precondition's state and whether the guard is Active, Dormant, or Disabled on this site.</li>\n\t\t</ul>\n\n\t\t<h4>1.2.1 &mdash; 6 June 2026</h4>\n\t\t<ul>\n\t\t  <li>Added a \"Repair CSS Combine\" toggle under a new <em>Other Settings</em> section (on by default) so the Combine Guard + Catcher can be switched off without deactivating the plugin.</li>\n\t\t</ul>\n\n\t\t<h4>1.2.0 &mdash; 6 June 2026</h4>\n\t\t<ul>\n\t\t  <li>New \"Combine Guard + Catcher\" feature (on by default): works around a LiteSpeed Cache 7.2&ndash;7.8.1 bug (lscache_wp#999) where older WordPress cores render the dummy CSS &lt;link&gt; with a double space, defeating LSCWP's combine regex and dropping the combined stylesheet (naked, CSS-less page). The Guard collapses the dummy tag's whitespace so combine injects normally; the no-cache Catcher refuses to cache any page where the dummy placeholder survived.</li>\n\t\t</ul>\n\n\t\t<h4>1.1.9 &mdash; 20 November 2022</h4>\n\t\t<ul>\n\t\t  <li>Added support for cases where no LiteSpeed JS is loaded but only CSS: user now sees warning to manually refresh the page.</li>\n\t\t</ul>\n\n\t\t<h4>1.1.8 &mdash; 21 September 2022</h4>\n\t\t<ul>\n\t\t  <li>Fixed improperly-scoped fallback processing.</li>\n\t\t  <li>Added resilience against WordPress Admin being targeted for fallback CSS handling.</li>\n\t\t</ul>\n\n\t\t<h4>1.0.1-1.1.7 &mdash; 16 July 2022</h4>\n\t\t<ul>\n\t\t  <li>Bug fix compatibility with our other plugins.</li>\n\t\t  <li>Allows for always-showing Loading message.</li>\n\t\t  <li>Added cache-busting versioning to CSS and JS assets.</li>\n\t\t  <li>Improved cache-proof verification prompts in plugin Toolbox.</li>\n\t\t</ul>\n\t\n\t\t<h4>1.0.0 &mdash; 8 August 2022</h4>\n\t\t<ul>\n\t\t  <li>Initial release.</li>\n\t\t</ul>","description":"Provides a dynamic fallback for 404-missing JS/CSS LiteSpeed-optimized assets. Inspired by Autoptimize's 404 fallback."},"author_profile":"https://profiles.wordpress.org/harryfear","name":"404 Fallback for LiteSpeed","requires":"4.8","author":"<a href='https://pph.me/harryfear'>Harry F.</a>","download_url":"https://plugins.skydesignltd.com/repos/404-litespeed-fallback/latest/404-litespeed-fallback.zip?ver=1.5.7","tested":"7.1"}