Skip to content

Keep a failing module from taking the application down - #326

Merged
jakub-przepiora merged 1 commit into
developfrom
fix/module-enable-lifecycle
Sep 28, 2026
Merged

jakub-przepiora merged 1 commit into
developfrom
fix/module-enable-lifecycle

Conversation

@jakub-przepiora

Copy link
Copy Markdown
Contributor

ModuleManager::loadEnabled() registers each enabled module's provider inside a try/catch, and the comment above it promises "a bad module never prevents the application from booting". It only half held.

Laravel calls a provider's boot() later, while booting every provider — long after loadEnabled() returned. An exception thrown there escapes that catch, and the module that threw takes every route with it.

How it showed up

A module built against a newer core called a menu helper with an argument this version does not accept (Unknown named parameter $badge). The result was a 502 on the whole installation, including the admin screen needed to disable the module again. Recovery meant deleting the module's directory by hand, as root, from inside the container.

The fix

Providers are wrapped in ModuleProviderGuard, which runs register() and boot() itself and swallows what either throws, logging it and passing it to report(). A module that fails contributes nothing — no routes, menus or listeners — and the rest of the application is untouched.

boot() is resolved through the container, the way Laravel calls it, so a provider that type-hints dependencies there still works.

One trap worth naming

The guard is registered with force: true, because the container deduplicates providers by class name. Every module's guard is the same class, so without it the second module's guard would be mistaken for the first one's and that module would never load at all.

That would have been a worse bug than the one being fixed: silent, and only visible once someone installed a second module. There is a test for it.

Tests

Five new cases in ModuleProviderGuardTest:

  • an exception from register() does not escape
  • an exception from boot() does not escape
  • a working provider still registers and boots
  • two modules both load despite sharing a guard class
  • a provider without boot() at all is fine

Full suite: 2952 passed, 2 failed. Both failures are in InstallPresetTest and were verified against this branch's base with git stash — they exist without this change.

Note

While investigating this I also expected enabling a module to need a manual restart for its routes to appear. That turned out to be wrong: ModulesController::enable() already calls clearCache(), which runs route:clear, config:clear and hooks an Octane worker reload to RequestTerminated. No change was needed there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QRWsLoNeVWdsSHve6vUmxs

ModuleManager::loadEnabled() registered each enabled module's provider inside
a try/catch, and the comment promised "a bad module never prevents the
application from booting". It only half held. Laravel calls a provider's
boot() later, while booting every provider, so an exception thrown there
escapes that catch: the module that threw takes every route with it.

Seen with a module built against a newer core — it called a menu helper with
an argument this version does not accept. The result was a 502 on the whole
installation, including the admin screen needed to disable the module again;
recovery meant deleting its directory by hand.

Providers are now wrapped in ModuleProviderGuard, which runs register() and
boot() itself and swallows what either throws, logging it and reporting it.
A module that fails simply contributes nothing — no routes, menus or
listeners — and the rest of the application is untouched.

The guard is registered with force: true because the container deduplicates
providers by class name. Every module's guard is the same class, so without
it the second module's guard would be mistaken for the first one's and that
module would never load at all — a quieter bug than the one being fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRWsLoNeVWdsSHve6vUmxs
@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • cla-signed

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: Mes-Open/OpenMes/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 7d9d4d97-7a2c-4940-957b-9ab6ab25febf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jakub-przepiora
jakub-przepiora merged commit 476f639 into develop Sep 28, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant