Skip to content

feat: allow replacement of the default serinus http adapter#227

Open
francescovallone wants to merge 4 commits into
mainfrom
feat/change_http_adapter
Open

feat: allow replacement of the default serinus http adapter#227
francescovallone wants to merge 4 commits into
mainfrom
feat/change_http_adapter

Conversation

@francescovallone
Copy link
Copy Markdown
Owner

@francescovallone francescovallone commented Mar 7, 2026

Description

Allow the user to change adapter to their application easily.

Type of change

  • ⭐ New feature (non-breaking change which adds functionality)
  • 📑 This change requires a documentation update

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • New Features

    • Inject custom HTTP adapters when creating applications (including minimal apps).
    • Replace or register adapters by key and ensure primary adapter is tracked.
    • Expose application initialization status via a public API.
  • Tests

    • Updated tests to exercise adapter replacement, lifecycle, and clean shutdown behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Introduces keyed adapter management and runtime replacement: AdapterContainer gains keyed add/replace APIs; ApplicationConfig tracks and can replace its server adapter and marks attachment; Application and factory functions accept optional adapters and propagate adapter configuration; tests use a tracking adapter to verify lifecycle and replacement.

Changes

Cohort / File(s) Summary
Adapter Container
packages/serinus/lib/src/containers/adapters_container.dart
Added primaryHttpAdapterKey constant; new addAs(String, Adapter) and replace(String, Adapter) methods; values getter now returns a Set; add(Adapter) delegates to addAs.
Serinus Container
packages/serinus/lib/src/containers/serinus_container.dart
Made applicationRef mutable (removed final) and call to config.attachToApplication() added in constructor initialization.
Application Core
packages/serinus/lib/src/core/application.dart
Added private _configureApplicationConfig(...), constructor now accepts optional HttpAdapter? adapter, added public isInitialized getter, updated initialize/shutdown flows to use isInitialized, propagated adapter param to MicroserviceApplication and SerinusApplication, removed direct serverAdapter.close() calls.
Application Config
packages/serinus/lib/src/core/application_config.dart
Replaced public serverAdapter field with private _serverAdapter plus serverAdapter getter/setter; added attachToApplication() and replaceServerAdapter(HttpAdapter); constructor now requires serverAdapter and registers it with adapters.addAs(AdapterContainer.primaryHttpAdapterKey, ...).
Factory & Minimal App
packages/serinus/lib/src/core/factory.dart, packages/serinus/lib/src/core/minimal/minimal_application.dart
createApplication and createMinimalApplication accept optional HttpAdapter? adapter and use it when provided; factories build a single ApplicationConfig and pass it (and adapter) through; SerinusMinimalApplication factory/ctor now accept and forward adapter.
Tests
packages/serinus/test/core/application_test.dart
Replaced mock with private _TrackingAdapter test double that tracks initialized, listened, closeCalls and throwOnListen; tests updated to assert adapter replacement, initialization/listen behavior, wiring into config/adapters, and single-close semantics.

Sequence Diagram

sequenceDiagram
    actor User
    participant Factory
    participant AppConfig as ApplicationConfig
    participant AdapterContainer
    participant SerinusApp as SerinusApplication
    participant HttpAdapter

    User->>Factory: createApplication(adapter: OptionalHttpAdapter)
    activate Factory
    alt adapter provided
        Factory->>HttpAdapter: use provided adapter
    else
        Factory->>HttpAdapter: construct SerinusHttpAdapter
    end
    Factory->>AppConfig: new ApplicationConfig(serverAdapter)
    activate AppConfig
    AppConfig->>AdapterContainer: addAs(primaryHttpAdapterKey, serverAdapter)
    activate AdapterContainer
    AdapterContainer->>AdapterContainer: store adapter under key
    deactivate AdapterContainer
    AppConfig->>AppConfig: attachToApplication()
    deactivate AppConfig
    Factory->>SerinusApp: new SerinusApplication(config, adapter?)
    activate SerinusApp
    SerinusApp->>HttpAdapter: init(config)
    activate HttpAdapter
    HttpAdapter-->>SerinusApp: initialized
    deactivate HttpAdapter
    deactivate SerinusApp
    deactivate Factory
    Factory-->>User: SerinusApplication ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through keys and swapped a thread,
Adapters tucked where configs are led.
From factory seed to app's first start,
I guard the swaps with nimble heart.
A fragrant patch — the connections thread.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically summarizes the main change: enabling users to replace the default Serinus HTTP adapter, which is the primary objective across all modified files.
Description check ✅ Passed The description provides a clear summary of the change, includes the feature type selection, and has a completed checklist confirming testing, style compliance, and documentation updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/change_http_adapter

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/serinus/lib/src/containers/adapters_container.dart`:
- Around line 32-35: The replace(String key, Adapter adapter) method currently
overwrites or inserts unconditionally into the _adapters map; change it to
ensure it only replaces existing entries: check _adapters.containsKey(key) and
if the key does not exist throw a clear error (e.g., StateError or
ArgumentError) instead of inserting, otherwise assign the adapter to
_adapters[key]; this prevents silent creation of a second entry when callers
meant to replace an existing adapter.

In `@packages/serinus/lib/src/core/application_config.dart`:
- Around line 120-123: The public serverAdapter setter allows callers to swap
adapters after Application construction which desynchronizes state; change the
setter on ApplicationConfig (serverAdapter) to prevent runtime swaps by either
making it private or by guarding it: detect if _container.applicationRef or the
cached RoutesResolver/lifecycle is already initialized and throw an error (or
no-op) instead of updating _serverAdapter and adapters.replace; ensure
references to _container.applicationRef, RoutesResolver, and the adapter
lifecycle are consulted so callers cannot change adapters via
app.config.serverAdapter after Application is constructed.

In `@packages/serinus/lib/src/core/application.dart`:
- Around line 73-80: The public isInitialized getter and the guard in
replaceHttpAdapter rely on a base _isInitialized flag, but
MicroserviceApplication.initialize() uses a separate _isInizialized field and
Application.initialize() sets _isInitialized too early; unify to a single
private flag (e.g. _isInitialized) used everywhere, remove the duplicate
_isInizialized, and ensure Application.initialize() only sets _isInitialized
after _container.init() (and any other startup steps) complete successfully so a
failed startup doesn't flip the flag; update
MicroserviceApplication.initialize() and any checks (including
replaceHttpAdapter(HttpAdapter)) to reference the unified _isInitialized.
- Around line 94-96: After swapping the primary adapter (setting
config.serverAdapter and _container.applicationRef and calling await
adapter.init(config)), rebuild the RoutesResolver so RouteResponseController
captures the new applicationRef; specifically, after the adapter init call
reinstantiate or refresh _routesResolver (the RoutesResolver instance that
constructs RouteResponseController) so reply/render/redirect use the new adapter
implementation instead of the old one.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62da0342-81df-4a2a-be51-31267008f295

📥 Commits

Reviewing files that changed from the base of the PR and between aa11874 and 7b4892a.

📒 Files selected for processing (6)
  • packages/serinus/lib/src/containers/adapters_container.dart
  • packages/serinus/lib/src/containers/serinus_container.dart
  • packages/serinus/lib/src/core/application.dart
  • packages/serinus/lib/src/core/application_config.dart
  • packages/serinus/lib/src/core/factory.dart
  • packages/serinus/test/core/application_test.dart

Comment thread packages/serinus/lib/src/containers/adapters_container.dart
Comment thread packages/serinus/lib/src/core/application_config.dart
Comment thread packages/serinus/lib/src/core/application.dart Outdated
Comment thread packages/serinus/lib/src/core/application.dart Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/serinus/lib/src/core/application.dart (2)

73-75: ⚠️ Potential issue | 🟠 Major

isInitialized returns incorrect value for MicroserviceApplication.

The getter reads _isInitialized from the base class, but MicroserviceApplication.initialize() (Lines 247-263) sets a separate _isInizialized field (Line 211). This means isInitialized will return false for an initialized MicroserviceApplication.

Consider removing _isInizialized from MicroserviceApplication and having it call super.initialize() or directly set the base class field.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/serinus/lib/src/core/application.dart` around lines 73 - 75, The
MicroserviceApplication has its own _isInizialized field that gets set in
MicroserviceApplication.initialize(), while the base class exposes isInitialized
that reads _isInitialized; remove the duplicate _isInizialized field from
MicroserviceApplication and either call super.initialize() from
MicroserviceApplication.initialize() or explicitly set the base class field
(_isInitialized) in that method so that the inherited isInitialized getter
reflects the correct initialized state (refer to
MicroserviceApplication.initialize(), isInitialized, _isInitialized, and
_isInizialized).

97-99: ⚠️ Potential issue | 🔴 Critical

_routesResolver is not rebuilt after swapping the adapter.

After updating config.serverAdapter and _container.applicationRef (Lines 97-98), the existing _routesResolver still holds a reference to the old adapter captured during its construction. This means reply, render, and redirect in RouteResponseController will continue using the old adapter.

,

♻️ Proposed fix to rebuild the resolver
     config.serverAdapter = adapter;
     _container.applicationRef = adapter;
+    _routesResolver = RoutesResolver(_container);
     await adapter.init(config);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/serinus/lib/src/core/application.dart` around lines 97 - 99, After
swapping the adapter (updating config.serverAdapter and
_container.applicationRef) and calling await adapter.init(config), rebuild the
routes resolver so RouteResponseController's reply/render/redirect use the new
adapter; locate where _routesResolver is constructed (the _routesResolver
symbol) and reinstantiate it or call the existing factory/init method that
creates it (so it captures the updated _container.applicationRef), ensuring this
occurs immediately after adapter.init completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/serinus/lib/src/core/application.dart`:
- Around line 89-99: The code closes currentAdapter (currentAdapter.isOpen ->
currentAdapter.close) before calling adapter.init(config), which can leave the
app without a working adapter if init throws; change the flow to initialize the
new adapter first (call adapter.init(config)) and only after successful init
replace config.serverAdapter and _container.applicationRef and then close the
old adapter (currentAdapter.close), or if you prefer to keep current order wrap
adapter.init(config) in a try/catch that on failure attempts to restore the old
adapter/state and logs an unrecoverable error; reference currentAdapter,
adapter.init(config), currentAdapter.close, config.serverAdapter and
_container.applicationRef when making the change.
- Around line 94-99: The code only preserves adapter.viewEngine when replacing
the HttpAdapter, which discards other mutable adapter state (notFoundHandler,
rawBody) defined on HttpAdapter; update the replacement logic around
adapter/viewEngine/currentViewEngine so you also copy adapter.notFoundHandler
and adapter.rawBody (and any other mutable fields on HttpAdapter) from the
previous adapter instance to the new adapter before assigning
config.serverAdapter/_container.applicationRef and calling adapter.init(config)
so those custom behaviors are preserved.

---

Duplicate comments:
In `@packages/serinus/lib/src/core/application.dart`:
- Around line 73-75: The MicroserviceApplication has its own _isInizialized
field that gets set in MicroserviceApplication.initialize(), while the base
class exposes isInitialized that reads _isInitialized; remove the duplicate
_isInizialized field from MicroserviceApplication and either call
super.initialize() from MicroserviceApplication.initialize() or explicitly set
the base class field (_isInitialized) in that method so that the inherited
isInitialized getter reflects the correct initialized state (refer to
MicroserviceApplication.initialize(), isInitialized, _isInitialized, and
_isInizialized).
- Around line 97-99: After swapping the adapter (updating config.serverAdapter
and _container.applicationRef) and calling await adapter.init(config), rebuild
the routes resolver so RouteResponseController's reply/render/redirect use the
new adapter; locate where _routesResolver is constructed (the _routesResolver
symbol) and reinstantiate it or call the existing factory/init method that
creates it (so it captures the updated _container.applicationRef), ensuring this
occurs immediately after adapter.init completes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a05a7d1a-df4e-4e74-b438-fe53d711ddad

📥 Commits

Reviewing files that changed from the base of the PR and between 7b4892a and ccf4db3.

📒 Files selected for processing (2)
  • packages/serinus/lib/src/containers/adapters_container.dart
  • packages/serinus/lib/src/core/application.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/serinus/lib/src/containers/adapters_container.dart

Comment thread packages/serinus/lib/src/core/application.dart Outdated
Comment thread packages/serinus/lib/src/core/application.dart Outdated
@francescovallone
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/serinus/lib/src/core/application_config.dart (1)

138-143: Consider documenting adapter cleanup responsibility.

The replaceServerAdapter() method updates the backing field and the adapter container but does not close the previous adapter. Per context snippet 3 (adapters_container.dart:33-39), replace() simply overwrites the map entry.

If the old adapter has been initialized, this could leak resources. Consider either:

  1. Closing the old adapter before replacement, or
  2. Documenting that callers are responsible for cleanup.

Given this is an @internal method used during pre-initialization configuration (before attachToApplication()), the adapter is unlikely to be open yet. If that's the intentional design, a brief doc comment clarifying this would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/serinus/lib/src/core/application_config.dart` around lines 138 -
143, replaceServerAdapter currently overwrites the stored adapter without
closing any previously set instance, which may leak resources; update
replaceServerAdapter (and references to _serverAdapter and
adapters.replace/AdapterContainer.primaryHttpAdapterKey) to either: 1) capture
the existing _serverAdapter, call its close/shutdown/dispose method if non-null
before assigning the new value and replacing the container entry, or 2) add a
clear doc comment above replaceServerAdapter stating that this method is only
used during pre-initialization (before attachToApplication) and therefore
callers are responsible for adapter lifecycle/cleanup; pick one approach and
apply consistently.
packages/serinus/lib/src/core/application.dart (1)

233-242: Verify adapter close order in MicroserviceApplication.close().

The close method iterates over config.adapters.values and then config.microservices. Since the primary HTTP adapter is registered in the adapters container via addAs(primaryHttpAdapterKey, ...), it will be closed during the config.adapters.values loop.

This is correct behavior, but if any microservice depends on the HTTP adapter still being open for cleanup coordination, there could be ordering issues. Consider documenting the shutdown order or ensuring microservices don't have such dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/serinus/lib/src/core/application.dart` around lines 233 - 242,
MicroserviceApplication.close() currently closes config.adapters.values before
config.microservices which will close the primary HTTP adapter (registered via
addAs(primaryHttpAdapterKey, ...)) prior to microservice cleanup; to fix, either
reorder shutdown so you iterate and await config.microservices close() before
closing config.adapters.values (keeping shutdown() call after both), or add an
explicit comment/documentation near the close() implementation that states the
shutdown order and warns microservices not to rely on adapters being available
during their close(); update the method body or add the inline doc next to
MicroserviceApplication.close(), config.adapters.values, config.microservices,
and addAs(primaryHttpAdapterKey) accordingly.
packages/serinus/test/core/application_test.dart (1)

16-23: Add a test case that exercises throwOnListen to verify error handling.

The throwOnListen parameter is implemented (line 48-50) but never set to true in any test, as evidenced by the // ignore: unused_element_parameter comment. Add a test that passes throwOnListen: true to verify the application handles SocketException during listen() gracefully, exercising the error handling path in SerinusApplication.serve() (lines 316-318).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/serinus/test/core/application_test.dart` around lines 16 - 23, Add a
new test in application_test.dart that constructs the test adapter with
throwOnListen: true (using the _TrackingAdapter constructor) and calls
SerinusApplication.serve(), asserting that a SocketException thrown during
listen() is handled gracefully (the serve call does not rethrow and the
application transitions to the expected non-started/error state); this exercises
the throwOnListen path and the SerinusApplication.serve() error handling for
SocketException.
packages/serinus/lib/src/containers/serinus_container.dart (1)

36-37: Consider making applicationRef final or document the reconstruction requirement.

Making applicationRef mutable creates a theoretical desync risk if the entire adapter reference is replaced after RoutesResolver is created. However, the current codebase shows only property-level mutations (e.g., viewEngine setter), which safely propagate because all holders share the same reference. Additionally, the Application.container setter already reconstructs RoutesResolver when needed.

If runtime adapter replacement is not required, mark applicationRef as final for clarity. Otherwise, document that replacing the adapter requires reconstructing the container via the setter, or ensure all access paths use the current reference (rather than caching it).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/serinus/lib/src/containers/serinus_container.dart` around lines 36 -
37, The field applicationRef on SerinusContainer should either be made final or
have clear documentation and safeguards about replacing the adapter: if adapter
replacement is not needed, change the declaration of applicationRef to be final
to prevent desyncs with RoutesResolver; otherwise, update docs/comments near
applicationRef and/or modify code paths that cache the adapter (e.g., anywhere
that constructs RoutesResolver using applicationRef) so they re-read the current
Application.container (see Application.container setter) or trigger
reconstruction of RoutesResolver when the adapter is swapped. Ensure the fix
references applicationRef, RoutesResolver, and the Application.container setter
so maintainers can find and apply the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/serinus/lib/src/containers/serinus_container.dart`:
- Around line 36-37: The field applicationRef on SerinusContainer should either
be made final or have clear documentation and safeguards about replacing the
adapter: if adapter replacement is not needed, change the declaration of
applicationRef to be final to prevent desyncs with RoutesResolver; otherwise,
update docs/comments near applicationRef and/or modify code paths that cache the
adapter (e.g., anywhere that constructs RoutesResolver using applicationRef) so
they re-read the current Application.container (see Application.container
setter) or trigger reconstruction of RoutesResolver when the adapter is swapped.
Ensure the fix references applicationRef, RoutesResolver, and the
Application.container setter so maintainers can find and apply the change.

In `@packages/serinus/lib/src/core/application_config.dart`:
- Around line 138-143: replaceServerAdapter currently overwrites the stored
adapter without closing any previously set instance, which may leak resources;
update replaceServerAdapter (and references to _serverAdapter and
adapters.replace/AdapterContainer.primaryHttpAdapterKey) to either: 1) capture
the existing _serverAdapter, call its close/shutdown/dispose method if non-null
before assigning the new value and replacing the container entry, or 2) add a
clear doc comment above replaceServerAdapter stating that this method is only
used during pre-initialization (before attachToApplication) and therefore
callers are responsible for adapter lifecycle/cleanup; pick one approach and
apply consistently.

In `@packages/serinus/lib/src/core/application.dart`:
- Around line 233-242: MicroserviceApplication.close() currently closes
config.adapters.values before config.microservices which will close the primary
HTTP adapter (registered via addAs(primaryHttpAdapterKey, ...)) prior to
microservice cleanup; to fix, either reorder shutdown so you iterate and await
config.microservices close() before closing config.adapters.values (keeping
shutdown() call after both), or add an explicit comment/documentation near the
close() implementation that states the shutdown order and warns microservices
not to rely on adapters being available during their close(); update the method
body or add the inline doc next to MicroserviceApplication.close(),
config.adapters.values, config.microservices, and addAs(primaryHttpAdapterKey)
accordingly.

In `@packages/serinus/test/core/application_test.dart`:
- Around line 16-23: Add a new test in application_test.dart that constructs the
test adapter with throwOnListen: true (using the _TrackingAdapter constructor)
and calls SerinusApplication.serve(), asserting that a SocketException thrown
during listen() is handled gracefully (the serve call does not rethrow and the
application transitions to the expected non-started/error state); this exercises
the throwOnListen path and the SerinusApplication.serve() error handling for
SocketException.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c48829b8-6df0-4f66-8b05-5c7d6c2d49a6

📥 Commits

Reviewing files that changed from the base of the PR and between ccf4db3 and 5166633.

📒 Files selected for processing (6)
  • packages/serinus/lib/src/containers/adapters_container.dart
  • packages/serinus/lib/src/containers/serinus_container.dart
  • packages/serinus/lib/src/core/application.dart
  • packages/serinus/lib/src/core/application_config.dart
  • packages/serinus/lib/src/core/minimal/minimal_application.dart
  • packages/serinus/test/core/application_test.dart

@Jack-Sparrow-7
Copy link
Copy Markdown

I've been benchmarking Serinus today and found something important — dart:io has a hard performance ceiling at exactly 98,303 req/sec, regardless of connections:

• 2000 conns peak: 98,303 req/sec
• 5000 conns peak: 98,303 req/sec ← identical
• 10000 conns peak: 98,303 req/sec ← identical

The bottleneck is provably dart:io's HTTP parser, not Serinus itself. Serinus's architecture is already perfect — AOT + isolates scales beautifully to 72k avg req/sec at 10k connections, beating Bun (34,276 req/sec) on a laptop.

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