fix: #3624691 Send the redirect interaction as a trusted response, so the visitor reaches the destination
RedirectInteraction::respond() handed the visitor to its configured URL with a plain RedirectResponse. Core replaces any redirect leaving this site that is not a trusted one with a 400 carrying its own sentence about trusted redirects, and logs an error every time. So the one thing this plugin exists for, handing a party to a partner site, a document signer or an off-site form, never happened in a browser: the visitor read a core error on the public dispatcher route and the log filled up one line per visit.
It now returns a TrustedRedirectResponse. What vouches for the destination is that an administrator wrote it on the step, and the scheme check already there is what holds it to somewhere a browser can be sent at all. Trusted also means cacheable, which this must not be, since with a return link the URL carries a capability token minted for one visitor's branch: the step route already declares no_cache, and the response now says max-age 0 as well, which covers the identity doorway rendering the same plugin on a route of its own.
RedirectInteractionTest could not see any of this: every case reads the response the plugin built, which is what it meant to send rather than what a visitor is served, and the check runs on the way out after the controller has returned. The new testTheRedirectSurvivesCoresExternalRedirectCheck runs core's own subscriber over the built response the way the kernel does. Without the fix it gets the 400 back.
Rebased on 1.x after !529 (merged) merged (clean, no overlap).
Audit rounds (5, closed on a clean round).
Round 2 mutated the fix. Reverting to a plain RedirectResponse turns the test red, as it should. Removing the max-age 0 line did not — and that line turns out to be load-bearing in a way the original commit understated.
A plain RedirectResponse is not a CacheableResponseInterface at all, so no cache layer could ever store it, whatever it said. A TrustedRedirectResponse is. This MR is therefore what makes the response cacheable, and the max-age 0 beside it is the mitigation for a hazard the MR itself introduces. With a return link the URL carries a signed capability minted for one visitor's own branch, so a stored copy is that visitor's link handed to whoever asks next. testTheHandOffDeclaresItselfUncacheable now holds it; against the unfixed code it fails on "not a CacheableResponseInterface", and against the mutant on -1 is identical to 0.
Round 3 checked the comment's reasoning against core and found it wrong, which is the correction most worth having here: no_cache and max-age cover different caches and neither stands in for the other.
DenyNoCacheRoutesis taggedpage_cache_response_policyand nothing else, so the dynamic page cache never consultsno_cache— its only response policy isDenyAdminRoutes.max-age 0is what stops it.- The internal page cache ignores
max-ageby design — core says so where it setsCache::PERMANENTand expires by tag — so only the step route'sno_cachekeeps it out, and that cache applies to the anonymous bearer visitor, who is exactly the one holding a capability link.
Read the old comment the other way round — that max-age 0 "says the same" — and dropping no_cache from the step route looks safe. It would put a capability-bearing redirect in the anonymous page cache permanently.
The sweep confirms this is still the only redirect in the module leaving the site: every other one is built from Url::fromRoute() or a signal URL on this host, and the payment hand-off goes to a kessai route here. Nothing type-checks the concrete response class except InteractionResponseTrait, whose instanceof RedirectResponse still matches through inheritance, so the return-remembering still fires.