Skip to content

fix(core): reset request state behind a plugin interface proxy - #681

Open
etienne-scherrer wants to merge 1 commit into
marko-php:developfrom
etienne-scherrer:fix/reset-behind-plugin-proxy
Open

etienne-scherrer wants to merge 1 commit into
marko-php:developfrom
etienne-scherrer:fix/reset-behind-plugin-proxy

Conversation

@etienne-scherrer

Copy link
Copy Markdown
Contributor

Problem

RequestStateResetter finds the services to clear between requests with resolvedInstances(ResettableInterface::class). When a plugin targets the interface a resettable service is bound to, the container stores the plugin's interface wrapper instead. That wrapper implements only the plugged interface and PluginInterceptedInterface, so it fails the instanceof ResettableInterface check, and the service behind it is never reset. In a long-running worker its state carries from one request into the next.

A concrete subclass proxy is not affected: it extends the service, stays resettable, and holds the state itself.

Impact today

I found no first-party configuration that leaks across requests on 0.13.0:

  • SessionInterface cannot be plugged: its $started { get; } property hook makes the generated wrapper fatal.
  • GuardInterface resolves through AuthManager, which resets the guards it built directly.
  • marko/debugbar plugs ConnectionInterface, but its wrapper is not a TransactionInterface, so resolving TransactionInterface throws before a transaction can be left open. That looks like a separate debugbar bug.

It does affect an application or module that plugs the interface of its own resettable service, and #680 depends on it: there CsrfTokenManagerInterface becomes a resettable singleton that remembers the token a request arrived with, and with a plugin on that interface the remembered token would stay valid for later requests on the same worker.

Change

  • RequestStateResetter unwraps PluginInterceptedInterface proxies, nested ones included, until it reaches a ResettableInterface, and resets that. A concrete subclass proxy is reached first and reset as before.
  • $except is matched against the service it reaches, so passing getPluginTarget() still keeps a wrapped service alive (the RefreshDatabase case).
  • testing.md says a wrapped service is reset too.

Tests

  • A resettable service behind a plugin interface proxy is reset (failed before the change: the stored value survived).
  • A proxy that wraps another proxy is unwrapped to the service (failed with a one-level unwrap).
  • A concrete subclass proxy is still reset itself (fails if the resetter always unwraps).
  • A wrapped service passed to $except is left alone.

pest --parallel in php:8.5-cli with the suite's extensions: 14176 passed, 449 skipped, 15 failed; the 15 (GdImageProcessorTest, GdImageProcessorLimitsTest, IntegrationVerificationTest, RepoManagementScriptsTest) fail the same way on a clean develop in that container. phpcs, php-cs-fixer --dry-run, docs:lint and phpstan: clean.

@github-actions github-actions Bot added the bug Something isn't working label Nov 3, 2026
@etienne-scherrer
etienne-scherrer marked this pull request as ready for review November 3, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant