Description
Summary
Since 4.2.12, Optml_Manager::close_buffer() re-arms a fresh capture buffer after processing the page. Any plugin that captures its own output buffer later in shutdown receives that empty buffer instead of its own. FacetWP does exactly this, so every facet refresh on a FacetWP "wp" template page now returns the full HTML page followed by a JSON payload with "template":"". FacetWP's client throws a JSON error and the loading spinner never clears.
This is a second symptom of the change discussed in #1149 (Groovy Menu) and introduced by #1132. The Groovy Menu fix in #1150 does not cover this case, since the problem here is the re-arm, not the flush of foreign buffers.
Environment
- Optimole 4.2.13 (reproduced on 4.2.12 and 4.2.13; fine before 4.2.12)
- FacetWP 4.5 with the FacetWP Elementor add-on 1.9.4
- Elementor 4.2.4 / Elementor Pro 4.2.3, Loop Grid with "Enable FacetWP" (FacetWP's "wp" auto-detect template mode)
- WordPress on WP Engine, PHP
output_buffering on
- Site URL available privately on request
- Page with an Elementor Loop Grid using "Enable FacetWP" and any facet, so FacetWP uses its "wp" template mode and POSTs
facetwp_refresh to the page URL.
- Optimole active with image replacement enabled.
- Change any facet value.
Observed
The refresh response has Content-Type: application/json but the body is the complete HTML document followed by FacetWP's JSON, with "template":"". Browser console:
SyntaxError: Unexpected non-whitespace character after JSON at position 1099
Root cause
FacetWP opens ob_start() at init and, in FacetWP_Request::inject_template() hooked to shutdown at priority 0, calls ob_get_clean() expecting its own buffer, extracts <body>…</body> into template, then wp_send_json().
Optimole's close_buffer() runs first at shutdown priority PHP_INT_MIN. It captures and processes the page, echoes the result (which lands in FacetWP's buffer), then calls start_capture_buffer() again. FacetWP's ob_get_clean() therefore returns the new empty buffer.
ob_get_status(true) captured at shutdown priority -1 during a FacetWP refresh:
| level |
handler |
buffer_used |
| 0 |
default output handler |
0 |
| 1 |
WpeCommon::filter_html_output |
0 |
| 2 |
default output handler (FacetWP, opened at init) |
208422 |
| 3 |
Optml_Manager::handle_buffer_fallback (re-armed) |
0 |
Workaround that resolves it
add_filter( 'optml_capture_at_shutdown', function ( $capture ) {
if ( function_exists( 'FWP' ) && ! empty( FWP()->request->is_refresh ) ) {
return false;
}
return $capture;
} );
### Step-by-step reproduction instructions
1. Page with an Elementor Loop Grid using "Enable FacetWP" and any facet, so FacetWP uses its "wp" template mode and POSTs `facetwp_refresh` to the page URL.
2. Optimole active with image replacement enabled.
3. Change any facet value.
### Screenshots, screen recording, code snippet or Help Scout ticket
_No response_
### Environment info
_No response_
### Is the issue you are reporting a regression
Yes, this is a regression.
Description
Summary
Since 4.2.12,
Optml_Manager::close_buffer()re-arms a fresh capture buffer after processing the page. Any plugin that captures its own output buffer later inshutdownreceives that empty buffer instead of its own. FacetWP does exactly this, so every facet refresh on a FacetWP "wp" template page now returns the full HTML page followed by a JSON payload with"template":"". FacetWP's client throws a JSON error and the loading spinner never clears.This is a second symptom of the change discussed in #1149 (Groovy Menu) and introduced by #1132. The Groovy Menu fix in #1150 does not cover this case, since the problem here is the re-arm, not the flush of foreign buffers.
Environment
output_bufferingonfacetwp_refreshto the page URL.Observed
The refresh response has
Content-Type: application/jsonbut the body is the complete HTML document followed by FacetWP's JSON, with"template":"". Browser console:SyntaxError: Unexpected non-whitespace character after JSON at position 1099Root cause
FacetWP opens
ob_start()atinitand, inFacetWP_Request::inject_template()hooked toshutdownat priority 0, callsob_get_clean()expecting its own buffer, extracts<body>…</body>intotemplate, thenwp_send_json().Optimole's
close_buffer()runs first atshutdownpriorityPHP_INT_MIN. It captures and processes the page, echoes the result (which lands in FacetWP's buffer), then callsstart_capture_buffer()again. FacetWP'sob_get_clean()therefore returns the new empty buffer.ob_get_status(true)captured atshutdownpriority -1 during a FacetWP refresh:Workaround that resolves it