Skip to content

Detect circular self-resolution instead of StackOverflowError - #423

Merged
escamoteur merged 2 commits into
flutter-it:mainfrom
muhammadkamel:fix/detect-circular-self-resolution
Sep 19, 2026
Merged

escamoteur merged 2 commits into
flutter-it:mainfrom
muhammadkamel:fix/detect-circular-self-resolution

Conversation

@muhammadkamel

Copy link
Copy Markdown
Contributor

Summary

  • Detect when a registration's factory re-enters the same type (common footgun: registerLazySingleton<Iface>(getIt.call) / untyped tear-offs) and throw a clear StateError instead of overflowing the stack.
  • Avoid eager $stackTrace interpolation in creation-error debug logging when GetIt.noDebugOutput is set (that path can make release crash dumps look "native").
  • Add regression tests and a short README note with the safe pattern (() => getIt<Impl>()).

Motivation

In production, registering an untyped tear-off of call/get as a factory for interface T recursively calls get<T>() until StackOverflowError. Some crash reporters then drown in StackTrace.toString work, which obscured the real DI bug.

Test plan

  • dart test test/circular_self_resolution_test.dart
  • dart test (full suite)
  • Confirm message points authors at () => getIt<Impl>() / named tear-offs

Fail fast with a StateError when a factory re-enters the same registration
(e.g. registerLazySingleton<Iface>(getIt.call)), and only interpolate the
catch stack when debug output is enabled.
Clear the creation-in-progress guard when the factory returns its Future,
not when that Future completes, so overlapping always-new/cached async
resolutions are not treated as circular self-resolution.
@muhammadkamel

Copy link
Copy Markdown
Contributor Author

Follow-up: fixed a Bugbot finding where overlapping getAsync on the same async factory was falsely treated as circular dependency. The creation guard now only covers the synchronous factory start (lazy async still holds until the pending creation finishes). Added regression tests.

escamoteur added a commit that referenced this pull request Sep 19, 2026
Throws a descriptive StateError instead of a StackOverflowError when a factory
re-enters get<T>() for the same registration (e.g. registerLazySingleton<Iface>(getIt.call)).
Plain factories are not guarded since recursive factories are legitimate.
Based on #423 by @muhammadkamel.
@escamoteur
escamoteur merged commit 7d2bf9a into flutter-it:main Sep 19, 2026
@escamoteur

Copy link
Copy Markdown
Member

Thanks a lot for this, and sorry for the long silence — the flutter_it packages weren't maintained for a few months.

Merged for 9.3.0 with two tweaks: the guard now only covers lazy singletons, cached factories and async singletons (plain registerFactory is left unguarded because a factory that recursively calls itself, e.g. to build a tree, is legitimate), and it's inlined as try/finally so there is no closure allocation on the creation path. Your error message and tests are kept; a few more were added for cycles between lazy singletons and for recursive factories.

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.

2 participants