Fix compatibility with TranslatePress - #180
Conversation
|
@LukePPx thank you very much for digging into this and putting together the PR for full TranslatePress support. I agree with it and we'll take it on for v1.8.0, but in generic form. Rather than switching strategies conditionally when TranslatePress is detected, we'll make the outermost buffer the default for everyone: the buffer will open unconditionally in the drop-in phase (advanced-cache.php), before any plugin or MU plugin loads. Two options for how to get there, whichever you prefer:
Either way, we'd love to have you test the 1.8.0 beta against your TranslatePress setup before release. Until then, your MU plugin is a solid interim workaround for your sites. Thank you again and kind regards, Philipp |
|
@ouun Seems like a good plan. We are keen to test 1.8.0. Just let me know, once it's ready. |
Open the capture buffer in the drop-in phase, before any plugin loads, so output-buffer post-processors (TranslatePress, HTML optimizers) nest inside it and their final HTML is what gets stored. A sentinel at the former buffer position (template_redirect, PHP_INT_MAX - 10) records the cache decision, sticky-negative across replays; the handler stores only on the end-of-request FINAL flush after a positive sentinel. Buffers cleaned or flushed mid-request by third parties, chunk overflows past the 5MB cap, mid-request fastcgi_finish_request() calls, and responses carrying Content-Encoding all pass through unstored. Rule-driven TTL, grace, and cache decisions are honored up to the final flush; a late bypass wins. Single code path, no escape hatch. New public API for extensions: millicache()->response()->is_storable(). Thanks to @LukePPx for the report, root-cause analysis, and initial patch (#180). Co-authored-by: Lukas Thoma <127091282+LukePPx@users.noreply.github.com>
|
Thank you again, @LukePPx, for raising this issue and for your PR. Both opt in to WordPress with For Composer projects, opt-in is per package: use the command Would be great if you could test it with TranslatePress on your side to confirm that it fixes it for you. |
|
@ouun nothing exciting to report. Both betas seem to work with all our wordpress plugins. |
fix: capture the outermost output buffer when TranslatePress is active
Summary
MilliCache opens its capture buffer very late — on
template_redirectat priorityPHP_INT_MAX - 10— which makes it the innermost output buffer. Any plugin that opens its own output buffer earlier (e.g. oninit) becomes the outer buffer and post-processes the HTML after MilliCache has already snapshotted it. MilliCache therefore caches the pre-processed page.One known victim currently is TranslatePress, which starts its translation buffer on
init(priority 0):Root cause
PHP flushes nested output buffers LIFO — innermost first. Because MilliCache opens last, its callback (
Response\Processor::process_output_buffer()) runs first and captures the page before the outer buffers have transformed it.Fix (opt-in, auto-enabled for TranslatePress)
The historical behaviour is kept as the default. Only when an
init-phase output-buffer post-processor is detected — currently TranslatePress — does MilliCache switch to opening its buffer as the outermost one, so the post-processor nests inside and MilliCache captures the final, fully processed HTML.How the strategy is chosen
Whether TranslatePress is active is only knowable once plugins have loaded, so the choice is deferred to
initatPHP_INT_MIN(which still runs before TranslatePress' owninitpriority-0 buffer):template_redirect(PHP_INT_MAX - 10) — behaviour is byte-for-byte unchanged, including thecheck_cache_decision()gate andOptions::apply_to_state()call.init, wrapping TranslatePress.Engine::run()no longer registers the buffer directly; it calls a newstart_capture()helper. A fresh cache hit stillexit()s inside the reader, so the capture buffer is only set up for a MISS or a stale-hit background regeneration.Applying rule overrides on the outermost path
On the outermost path the buffer opens before WP-typed rules have resolved, so
Response\Processor::process_output_buffer()now folds the rule-driven TTL/grace/decision overrides into the state at flush time (Options::apply_to_state()) and honours a rule-driven bypass decided after the buffer opened (returns the output unstored). This runs at end-of-request once every hook has fired, so nothing is missed. On the default path the options were already applied attemplate_redirect, so re-applying is a harmless no-op.Detection & override
class_exists( 'TRP_Translate_Press', false )is reliable atinit— TranslatePress' main class is loaded during the plugin include, beforeplugins_loaded.millicache_capture_outermost_bufferlets integrators force the outermost strategy for other output-buffer post-processors, or disable it entirely:Admin and AJAX contexts are explicitly excluded, so only genuine front-end page renders use the outermost buffer. REST, XML-RPC, non-GET/HEAD and CLI requests are already short-circuited in the PHP-phase rules before
run(), so they never reach this code.Why this is safe
init-phase post-processor is present, the code path, hooks and priorities are identical to today.plugins_loaded/init/template_redirect/wp, all before the buffer flushes at shutdown. Reading options at flush is at least as late as the oldtemplate_redirectread, so no override is missed.Engine::start()still gates whetherrun()is called at all (XML-RPC, REST, non-GET/HEAD, CLI, files, nocache cookies/paths).DONOTCACHEPAGEdecisions are enforced in the flush callback on the outermost path; a bypassed request simply passes its output through unstored.Cache\Manager::cache_output()validates status viaWriter::should_cache(), so a 3xx captured by the earlier buffer is not stored.Test plan
Environment: WordPress + TranslatePress (default
en_US+ a secondary language, e.g./de/), MilliCache active with the drop-in installed./de/some-page/twice → first translated, second (HIT) default language. ❌/de/some-page/twice → both translated; debug header showsmissthenhit. ✅/some-page/still caches and serves correctly./de/and/produce distinct cache entries (request hash already includes the path).template_redirect(default path unchanged) and everything caches as before.DONOTCACHEPAGEon a translated page →x-millicache: bypass, nothing stored, page still translated.admin-ajax.phpand REST requests are not wrapped by the outermost buffer (still translated by TranslatePress' own buffer).__return_falsereproduces the old bug (confirms toggle);__return_trueforces the outermost path without TranslatePress.needs_outermost_buffer()is false by default / whenis_admin(), and true when the class exists on a front-end request.Notes for maintainers
@sincetags use1.7.8; adjust to your release-please cut. Conventional-commit title suggested above (fix:).src/Engine.php(run()+ newstart_capture()/needs_outermost_buffer()) andsrc/Engine/Response/Processor.php(process_output_buffer()).init-phase post-processors if you prefer.