Skip to content

Resolve shelf.io.connection_info lazily - #542

Merged
auto-submit[bot] merged 5 commits into
masterfrom
fix/lazy-connection-info
Oct 1, 2026
Merged

auto-submit[bot] merged 5 commits into
masterfrom
fix/lazy-connection-info

Conversation

@kevmoo

@kevmoo kevmoo commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

shelf_io resolves the shelf.io.connection_info context value lazily instead
of while constructing every Request.

dart:io does not cache HttpRequest.connectionInfo. Each call constructs a
fresh _HttpConnectionInfo by reading remoteAddress and remotePort off the
socket (_HttpConnectionInfo.create in sdk/lib/_http/http_impl.dart), so
building it eagerly spent two getpeername calls on every request — including
the majority that never read the value. #37, which added the field, asked for it
for request logging, which most handlers do not do per request.

Effect

200 requests, a fresh connection each (Connection: close), counted with
strace -f -e trace=getpeername,getsockname. The handler either ignores the
context value or reads remotePort from it.

build getpeername per request getsockname
master, ignored 402 2.000 7 total
master, read 402 2.000 7 total
this PR, ignored 0 0.000 7 total
this PR, read 400 2.000 7 total

A handler that ignores the value pays nothing; one that reads it pays exactly
what it paid before. On a small-response workload where shelf_io issues ~14.8
syscalls/request, this removes ~13% of them for handlers that do not use the
value.

getsockname is 7 calls total in every arm — fixed startup cost, not
per-request. socket.port is served from cache and never reaches the kernel.

Implementation

The context value is a private _LazyHttpConnectionInfo implements HttpConnectionInfo holding the HttpRequest. It resolves all three fields
together on first access and caches the result.

HttpConnectionInfo is an interface with exactly three getters
(remoteAddress, remotePort, localPort), and the SDK's implementation is
private, so no caller can be depending on a concrete type.

If the connection is already gone, HttpRequest.connectionInfo is null and
the ! throws. That error now surfaces when a handler reads a field rather than
while the adapter builds the Request.

Tests

Existing coverage passes unchanged, including the isA<HttpConnectionInfo>()
assertion. One test added, covering remotePort (not previously asserted),
stability across repeated reads, and survival through Request.change() — since
middleware copies context, that is where a lazy value would break.

dart analyze clean, dart format clean, 232 tests pass.

`dart:io` does not cache `HttpRequest.connectionInfo`: every call builds a
fresh `_HttpConnectionInfo` by reading `remoteAddress`, `remotePort` and
`port` off the socket (`http_impl.dart`), which costs two `getpeername`
calls and one `getsockname`.

`shelf_io` read it while constructing every `Request`, so those syscalls
were spent on every request including the majority that never look at the
connection info. It is documented as being there for logging, which most
handlers do not do per-request.

The context value is now a lazy `HttpConnectionInfo` that resolves all
three fields on first access and caches them. A handler that reads it
pays exactly what it paid before; one that does not pays nothing. The
public type is unchanged, since `HttpConnectionInfo` is an interface with
exactly these three getters and the SDK's implementation is private.

Behavior change worth noting: if the connection is already gone, the
error now surfaces when a field is read rather than while the `Request`
is being built.
Measured rather than inferred. Over 200 requests, fresh connection each,
counting with strace:

| build            | getpeername | per request | getsockname |
| :--------------- | ----------: | ----------: | ----------: |
| master, ignored  |         402 |       2.000 |     7 total |
| master, read     |         402 |       2.000 |     7 total |
| lazy, ignored    |           0 |       0.000 |     7 total |
| lazy, read       |         400 |       2.000 |     7 total |

The previous commit message and doc comment said the cost was two
`getpeername` plus one `getsockname`, reasoned from reading
`_HttpConnectionInfo.create`, which does touch `socket.port`. That was
wrong: `getsockname` is 7 calls total in every arm, a fixed startup cost,
so `socket.port` is served from cache and never reaches the kernel.

The saving is exactly two `getpeername` per request that does not read
the connection info, and no change for one that does.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request optimizes 'shelf_io' by lazily resolving the 'shelf.io.connection_info' context value. Instead of eagerly reading 'HttpRequest.connectionInfo' for every request, which incurs expensive syscalls, a new '_LazyHttpConnectionInfo' class is introduced to resolve and cache the connection fields on first access. A test has been added to verify this behavior. The feedback suggests replacing the null-assertion operator in '_LazyHttpConnectionInfo' with a descriptive 'StateError' to improve debuggability when the connection is already closed.

Comment thread pkgs/shelf/lib/shelf_io.dart Resolved
@kevmoo

kevmoo commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

One design question I would like an opinion on before this leaves draft.

When the connection is already gone, HttpRequest.connectionInfo returns null
and the ! throws. On master that happens while the adapter builds the
Request, so it surfaces as a 500 from shelf_io. With this change it happens
inside the handler, at the point a field is read.

I kept ! to stay closest to current behavior rather than introduce a new error
type, but two alternatives seem reasonable:

  1. Throw a StateError explaining the connection is gone, which is a better
    message than Null check operator used on a null value but is a new error
    type for this path.
  2. Have the getters return a sentinel instead of throwing, which never breaks a
    handler but can silently hand back a wrong address.

Happy to take whichever you prefer, or leave it as is.

The null-assertion operator threw `Null check operator used on a null
value`, which said nothing about the cause. That mattered less when the
throw happened inside the adapter while building the `Request`, because
no handler ever saw it. Now that it can surface in handler code, the
message is worth being explicit about.
@kevmoo
kevmoo marked this pull request as ready for review September 25, 2026 22:33
- Null out `_request` in `_LazyHttpConnectionInfo` once `_resolved` is
  cached so retaining the `HttpConnectionInfo` instance does not pin the
  underlying `HttpRequest` / `_HttpConnection` graph.
- Document in `shelf_io.dart` that middleware reading `connectionInfo`
  after an `await` gap should resolve a field beforehand or handle
  `StateError` if the connection closes mid-flight.
- Add a unit test in `test/shelf_io_test.dart` verifying that reading
  `remoteAddress`, `remotePort`, or `localPort` after the connection
  closes throws `StateError`.
@kevmoo
kevmoo requested a review from jonasfj September 26, 2026 00:36
@kevmoo

kevmoo commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

Answering my own question above, so it is not left open for a reviewer.

Option 1 was taken in c1789d0: a StateError with a message, rather than the
bare null-check. The failure is easier to act on now that it can surface inside
handler code instead of only inside the adapter.

b5d339c then went further and fixed something I had missed — retaining the
HttpConnectionInfo also pinned the HttpRequest / _HttpConnection graph for
the lifetime of the Request. _request is now cleared once the fields are
cached, with a test covering the closed-connection path.

So the three options in my earlier comment are settled: StateError, not the
sentinel, not the bare !. Happy to revisit if you would rather this not
introduce a new error type on that path.

Current state: 3 commits, all checks green, 40 tests passing locally.

Comment thread pkgs/shelf/lib/shelf_io.dart Resolved
@auto-submit
auto-submit Bot merged commit b2cb503 into master Oct 1, 2026
27 checks passed
@auto-submit
auto-submit Bot deleted the fix/lazy-connection-info branch October 1, 2026 05:53
copybara-service Bot pushed a commit to dart-lang/sdk that referenced this pull request Oct 7, 2026
Revisions updated by `dart tools/rev_sdk_deps.dart`.

core (https://github.com/dart-lang/core/compare/08e4022..4d80610):
  4d80610a  Thu Oct 1 22:03:38 2026 +0000  dependabot[bot]  Bump the github-actions group with 5 updates (dart-lang/core#1005)

dartdoc (https://github.com/dart-lang/dartdoc/compare/2a040a8..5eb0f2c):
  5eb0f2c7  Thu Oct 1 19:03:48 2026 +0000  dependabot[bot]  Bump github/codeql-action/upload-sarif from 4.37.9 to 4.38.2 in the github-actions group (dart-lang/dartdoc#4291)

http (https://github.com/dart-lang/http/compare/d8e8e9c..585d433):
  585d433  Mon Oct 5 16:12:54 2026 -0700  Brian Quinlan  fix(cupertino_http): fix a websocket memory leak (dart-lang/http#1991)
  337adf3  Mon Oct 5 16:03:25 2026 -0700  Brian Quinlan  fix(cupertino_http): add `autoReleasePool` use to `CupertinoClient` (dart-lang/http#1990)
  47c57df  Tue Sep 29 14:07:01 2026 -0700  Brian Quinlan  fix(ok_http): fix `NetworkOnMainThreadException` (dart-lang/http#1995)
  d8cde5d  Tue Sep 29 14:06:44 2026 -0700  Brian Quinlan  fix test (dart-lang/http#1996)
  31056fe  Tue Sep 29 13:57:20 2026 -0700  Brian Quinlan  chore(cronet_http): prepare to release 1.10.0 (dart-lang/http#1993)
  9bc3a91  Tue Sep 29 13:53:25 2026 -0700  Brian Quinlan  fix(cronet_http): one global reference per `onReadCompleted` (dart-lang/http#1992)
  49ecd36  Mon Sep 28 14:20:26 2026 -0700  Brian Quinlan  fix(conformance): `supportsAbort` not considered in response (dart-lang/http#1994)

native (https://github.com/dart-lang/native/compare/27f7678..a2a6179):
  a2a617910  Tue Oct 6 13:53:17 2026 +0200  Daco Harkes  [jni_flutter] Bump version for publishing (dart-lang/native#3730)
  d3a99d66b  Tue Oct 6 19:46:34 2026 +1100  Liam Appelbe  [infra] Fix ffigen and jni_flutter (dart-lang/native#3724)
  b66d0815a  Tue Oct 6 04:28:35 2026 +0300  Manar Elhabbal  [ffigen] add Support passing C++ classes by value  (dart-lang/native#3677)
  f7ffe9667  Fri Oct 2 11:33:49 2026 +0200  Moritz  [native_toolchain_c] Don't pass /INCLUDE per symbol with a generated .def on Windows (dart-lang/native#3718)
  77a5e4169  Thu Oct 1 10:56:45 2026 +0300  Hassnaa Mohamed  [ffigen] Fix cpp method filtering (dart-lang/native#3696)
  cca065ca6  Thu Oct 1 10:17:12 2026 +1000  Liam Appelbe  [infra] Prepare to publish everything (dart-lang/native#3716)
  f13623ee3  Thu Oct 1 10:16:39 2026 +1000  Liam Appelbe  [jnigen] API docs (dart-lang/native#3715)
  be618790c  Thu Oct 1 10:05:26 2026 +1000  Liam Appelbe  [objective_c] API docs (dart-lang/native#3708)
  a18dd1148  Thu Oct 1 10:04:38 2026 +1000  Liam Appelbe  [ffigen] API docs (dart-lang/native#3707)
  6fd87c3b2  Tue Sep 29 19:01:11 2026 -0700  Brian Quinlan  [jni] Clarify `JBuffer.asUint8List` behavior (dart-lang/native#3704)
  6d5bbcfad  Wed Sep 30 09:35:47 2026 +1000  Liam Appelbe  [jni] API documentation (dart-lang/native#3706)
  fbcbc894b  Wed Sep 30 09:35:34 2026 +1000  Liam Appelbe  [ffigen] Document autorelease issues (dart-lang/native#3705)
  f99449ec9  Tue Sep 29 09:39:27 2026 +1000  Liam Appelbe  [infra] Add swift2objc to the workspace (dart-lang/native#3700)
  31494fa08  Tue Sep 29 09:38:59 2026 +1000  Liam Appelbe  [infra] Add jni_util to the workspace (dart-lang/native#3698)

shelf (https://github.com/dart-lang/shelf/compare/e5c8dc6..b2cb503):
  b2cb503  Wed Sep 30 22:53:07 2026 -0700  Kevin Moore  Resolve shelf.io.connection_info lazily (dart-lang/shelf#542)
  b70fafa  Tue Sep 29 13:37:08 2026 -0700  Kevin Moore  Match parameterless shelf_router routes without running a regex (dart-lang/shelf#544)

test (https://github.com/dart-lang/test/compare/4f92d53..d749b10):
  d749b104  Mon Oct 5 15:42:46 2026 -0700  Nate Bosch  Fail isolate suites when the test isolate exits (dart-lang/test#2775)
  da2157e9  Mon Oct 5 11:12:43 2026 -0700  Nate Bosch  Wait longer for the Chrome test tab to appear (dart-lang/test#2771)
  00bd0c99  Mon Oct 5 18:50:03 2026 +0300  Yusuf İhsan Görgel  Fix broken links in architecture.md (dart-lang/test#2774)
  3be2f547  Thu Oct 1 03:46:29 2026 +0000  dependabot[bot]  Bump the github-actions group with 4 updates (dart-lang/test#2773)
  40a5ad90  Tue Sep 29 12:07:31 2026 -0700  Nate Bosch  Delete compilation artifacts once they are no longer needed (dart-lang/test#2764)
  171ad0e5  Mon Sep 28 17:29:48 2026 -0700  Jacob MacDonald  export additional types from package:test_api/backend.dart (dart-lang/test#2745)
  52b45064  Mon Sep 28 15:04:08 2026 -0700  Nate Bosch  Tag browser and node tests, relax flaky timeouts (dart-lang/test#2770)

tools (https://github.com/dart-lang/tools/compare/d87eaf7..a66d517):
  a66d517d  Tue Oct 6 09:41:32 2026 -0700  Phil Quitslund  [analytics] `DASH__SUPPRESS_ANALYTICS` env handling (dart-lang/tools#2641)
  00c6e4ad  Fri Oct 2 17:17:49 2026 +0530  shivytyahoo  Fix VersionConstraint.intersection throwing on disjoint ranges (dart-lang/tools#2622)
  d523e1f8  Thu Oct 1 11:09:16 2026 +0200  Morgan :)  [unified_analytics] Append to the log file instead of rewriting it (dart-lang/tools#2613)
  6993cbad  Wed Sep 30 14:09:32 2026 +0300  Yusuf İhsan Görgel  [yaml] Improve error messages when mixing list and key-value syntax (dart-lang/tools#2610)
  068119b4  Tue Sep 29 23:21:03 2026 +0200  Lukas Klingsbo  [pubspec_parse] Support tag_pattern for git dependencies (dart-lang/tools#2565)
  7d859ed5  Mon Sep 28 20:28:23 2026 -0700  Kevin Moore  [api_summary] Add expectApiSummaryClean and --write/--check CLI flags (dart-lang/tools#2603)

web (https://github.com/dart-lang/web/compare/ec71a4c..63e6662):
  63e6662  Sat Oct 3 00:23:21 2026 +0000  Natalie Weizenbaum  Add a JSPair type (dart-lang/web#586)
  2d68d90  Fri Oct 2 20:26:48 2026 +0000  Natalie Weizenbaum  Use `js_interop` APIs in `js_interop_gen` (dart-lang/web#585)
  037823c  Thu Oct 1 21:59:21 2026 +0000  Natalie Weizenbaum  Add JSMap, JSSet, and JSSetLike types (dart-lang/web#571)
  b2d1d6b  Thu Oct 1 06:57:03 2026 +0000  dependabot[bot]  Bump the github-actions group with 3 updates (dart-lang/web#584)
  8dc8f75  Wed Sep 30 08:12:18 2026 -0700  Kevin Moore  [js_interop_gen] Split union/intersection AST types and extract TypeTransformer (dart-lang/web#580)
  751ccdf  Wed Sep 30 08:12:18 2026 -0700  Kevin Moore  [js_interop_gen] Decompose `Transformer._transformType` and deduplicate type caching (dart-lang/web#579)
  6f3dd4e  Wed Sep 30 08:12:17 2026 -0700  Kevin Moore  [js_interop_gen] Deduplicate `UnionType` and `IntersectionType` via `UnionOrIntersectionType` (dart-lang/web#578)
  d9f095e  Tue Sep 29 12:17:29 2026 -0700  Kevin Moore  [js_interop_gen] Remove unused JS interop extension types and bindings (dart-lang/web#577)
  07111ee  Tue Sep 29 09:51:36 2026 -0700  Kevin Moore  fix(js_interop_gen): update js_type_supertypes for Dart 3.12 (dart-lang/web#582)

Change-Id: I4532049c45561fe8f8c57713ac8ec2153096fd9a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/560660
Commit-Queue: Nate Bosch <[email protected]>
Reviewed-by: Brian Quinlan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants