Skip to content

bottom shelf - #517

Draft
kevmoo wants to merge 106 commits into
masterfrom
bottom_shelf
Draft

bottom shelf#517
kevmoo wants to merge 106 commits into
masterfrom
bottom_shelf

Conversation

@kevmoo

@kevmoo kevmoo commented Apr 20, 2026

Copy link
Copy Markdown
Member
  • pkg:bottom_shelf
  • classes final to start with
  • constants!
  • modern formatting!
  • Modern lints!
  • mono-repo!

@github-actions github-actions Bot added the type-infra A repository infrastructure change or enhancement label Apr 20, 2026

@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 introduces bottom_shelf, a high-performance Shelf adapter that interfaces directly with raw sockets. It features a custom HTTP/1.1 parser using byte slices, lazy header parsing, and a dedicated response serializer. The review feedback identifies several critical issues in the initial implementation, particularly regarding request body handling: the current logic fails to support bodies spanning multiple TCP chunks, incorrectly handles keep-alive connections when a body is present, and loses data during socket hijacking. There are also concerns about the FixedLengthBodyStream not properly draining for keep-alive or preserving pipelined data, and a recommendation to replace print statements with a proper logging mechanism.

Comment thread pkgs/bottom_shelf/lib/src/raw_shelf_server.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/raw_shelf_server.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/raw_shelf_server.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/body_stream.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/body_stream.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/raw_shelf_server.dart Outdated
@kevmoo

kevmoo commented Apr 21, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@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 introduces bottom_shelf, a high-performance Shelf adapter designed to interface directly with raw sockets. It features a custom byte-slice HTTP/1.1 parser, support for chunked and fixed-length request/response bodies, and various security mitigations. Review feedback identified several critical areas for improvement, including a potential integer overflow in chunked body parsing, a race condition in header slice management during asynchronous request dispatch, and multiple RFC 9112 compliance issues related to header whitespace handling, mandatory Host headers for HTTP/1.1, and URI construction.

Comment thread pkgs/bottom_shelf/lib/src/body_stream.dart
Comment thread pkgs/bottom_shelf/lib/src/http_connection.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/raw_http_parser.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/http_connection.dart Outdated
Comment thread pkgs/bottom_shelf/lib/src/http_connection.dart
@kevmoo

kevmoo commented Apr 21, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@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 introduces bottom_shelf, a high-performance Shelf adapter that interfaces directly with raw sockets. It features a custom HTTP/1.1 parser using byte slices, streaming controllers for fixed-length and chunked request bodies, and a response serializer supporting chunked encoding. The implementation includes comprehensive documentation and a robust test suite covering security and protocol compliance. Review feedback highlights several opportunities to improve RFC 9112 adherence, including stricter validation of the Host, Connection, and Transfer-Encoding headers, as well as more rigorous parsing of header keys and lines to mitigate potential request smuggling and injection vulnerabilities. Additionally, the laziness of the header mapping was noted as a potential performance bottleneck due to how the shelf.Request constructor interacts with it.

Comment thread pkgs/bottom_shelf/lib/src/raw_http_parser.dart
Comment thread pkgs/bottom_shelf/lib/src/typed_headers.dart Outdated
Comment on lines +76 to +89
bool get isChunked {
if (_cache case {_CacheKey.isChunked: final bool value}) return value;
for (var slice in _slices) {
if (slice.key.matches($Header.transferEncoding)) {
final value = slice.value.asString().toLowerCase();
if (value.contains('chunked')) {
_cache[_CacheKey.isChunked] = true;
return true;
}
}
}
_cache[_CacheKey.isChunked] = false;
return false;
}

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.

medium

According to RFC 9112 Section 6.1, if a Transfer-Encoding header field is present, chunked must be the final encoding. The current implementation uses value.contains('chunked'), which is too permissive and could be bypassed if other encodings are appended after chunked.

Comment thread pkgs/bottom_shelf/lib/src/typed_headers.dart Outdated
Comment on lines +34 to +44
CaseInsensitiveMap<List<String>> get _map {
if (_inner == null) {
final map = _inner = CaseInsensitiveMap<List<String>>();
for (var slice in _slices) {
map
.putIfAbsent(slice.key.asString(), () => [])
.add(slice.value.asString());
}
}
return _inner!;
}

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.

medium

The LazyByteHeaderMap is not truly lazy when used with package:shelf. The shelf.Request constructor immediately iterates over the provided headers to internalize them, which triggers the _map getter and thus the hydration of the entire map and all string allocations. This defeats the performance benefits of using byte slices. Consider if a different approach to header handling is possible or acknowledge this limitation in the documentation.

Comment thread pkgs/bottom_shelf/lib/src/raw_http_parser.dart Outdated
Comment on lines +142 to +143
(_buffer[start] == $Chars.sp ||
_buffer[end - 1] == $Chars.sp)) {

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.

medium

The check for invalid whitespace in the header key should also include the horizontal tab character (HTAB, ASCII 9), as RFC 9112 forbids any whitespace between the field name and the colon.

Suggested change
(_buffer[start] == $Chars.sp ||
_buffer[end - 1] == $Chars.sp)) {
(_buffer[start] == $Chars.sp || _buffer[start] == 9 ||
_buffer[end - 1] == $Chars.sp || _buffer[end - 1] == 9)) {

Comment on lines +82 to +95
static String _getStatusPhrase(int statusCode) => switch (statusCode) {
200 => 'OK',
201 => 'Created',
204 => 'No Content',
301 => 'Moved Permanently',
302 => 'Found',
304 => 'Not Modified',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
500 => 'Internal Server Error',
_ => 'Unknown',
};

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.

medium

The list of status phrases is incomplete. While the status code is the primary indicator, providing 'Unknown' for common codes (e.g., 206 Partial Content, 405 Method Not Allowed, 429 Too Many Requests) is not ideal for client compatibility. Consider expanding this list or using a more comprehensive mapping.

kevmoo added 9 commits April 21, 2026 14:17
- Created `pkgs/_shelf_compliance` package to run HTTP/1.1 compliance tests.
- Vendored the `Http11Probe` tool as a git submodule in `vendor/Http11Probe`.
- Implemented a generic test harness in `test/compliance_test.dart` that runs the probe by category and verifies against goldens.
- Added aggressive pruning to the JSON reports (removing skips, summary blocks, durations, and dynamic flags like `doubleFlush`) and saved them directly as JSON arrays.
- Created `tool/generate_summary.dart` to post-process JSON reports into a clean, deduplicated markdown summary file (`shelf_summary.md`).
- Added `mono_pkg.yaml` and configured a separate `compliance` workflow in `.github/workflows/compliance.yml`.
- Added `tool/run_tests_with_dotnet.sh` to pre-install the .NET 10 SDK on CI runners before running tests.
- Documented the package in its own README and linked to the summary from the root README.
@kevmoo

kevmoo commented Apr 22, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@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 introduces the _shelf_compliance package, which provides HTTP/1.1 compliance and hardening tests for the shelf ecosystem using the Http11Probe tool. It includes echo server implementations, test runner logic, and baseline reports for various RFC compliance categories. Feedback suggests using request.headersAll instead of request.headers in the echo handler to ensure that multiple instances of headers, such as Cookie, are processed as distinct values rather than being automatically joined into comma-separated strings, which is critical for accurate compliance verification.

Comment thread pkgs/_shelf_compliance/lib/echo_handler.dart
Comment thread pkgs/_shelf_compliance/lib/echo_handler.dart
@kevmoo
kevmoo changed the base branch from master to compliance April 22, 2026 14:26
Base automatically changed from compliance to master April 23, 2026 06:15
@kevmoo

kevmoo commented Apr 24, 2026

Copy link
Copy Markdown
Member Author

Here is the pivoted table with the rows combining Path and Compile Mode, showing how much faster bottom_shelf is compared to the other two:

Test Case shelf_io bottom_shelf dart:io
/text (JIT) 19789.76 (+103.1%) 40186.61 29694.00 (+35.3%)
/text (AOT) 26805.77 (+136.0%) 63258.37 47256.03 (+33.9%)
/headers (JIT) 17464.80 (+115.6%) 37652.65 30671.37 (+22.8%)
/headers (AOT) 32594.13 (+90.6%) 62130.32 47491.44 (+30.8%)
/stream (JIT) 845.74 (+78.5%) 1510.01 1278.17 (+18.1%)
/stream (AOT) 2135.59 (+12.6%) 2405.46 2110.89 (+14.0%)

(Note: The percentages in parentheses show how much faster bottom_shelf is compared to that specific baseline. For example, in /text (JIT), bottom_shelf is 103.1% faster than shelf_io.)

Key Takeaways:

  • bottom_shelf is at least 90% faster than shelf_io on text and headers in all modes.
  • bottom_shelf is 22% to 35% faster than raw dart:io on text and headers.
  • The gap narrows in the /stream AOT test, but bottom_shelf still maintains a ~13% lead over both.

lrhn and others added 14 commits April 27, 2026 10:05
* Optimize hex parsing.

The biggest improvement comes from using a `const` string as table. That avoids (potentially) a lazy initialization check on accessing the `_charFlags`. Also, a constant string may have a fixed location in memory.

Negating the "is valid hex" and putting the bit in bit 7 allows using `entry.toSigned(8)`, which a smart compiler should be able to turn into a single sign-extending opcode.
(Using `(entry & 0x0F) - (entry & 0x80)` works too.)

The `missing_whitespace_between_adjacent_strings` lint doesn't understand that not all strings are text.
Maybe it shouldn't trigger for strings containing character escapes.
Relocate runZonedGuarded from per-request dispatch up to connection
handler, eliminating 10,000+ Zone allocations and transitions per
second under keep-alive loads while preserving async error teardown.
…zyByteHeaderMap

Implement package:shelf Headers contract directly on LazyByteHeaderMap
and add zero-allocation ASCII mixed-case matching to HeaderByteSlice,
bypassing header table copying on shelf.Request construction.
… response buffers

Cache RFC 1123 Date header string per second and combine HTTP status
line, headers, and known response bodies into a single buffer, reducing
outbound TCP socket write syscalls to 1 per response.
…alysis

Document zero-copy parser state machine, keep-alive pipelined flow,
benchmark improvements (+14% RPS), and future zero-allocation proposals.
- Bind benchmark servers to 127.0.0.1 explicitly ('localhost' resolved
  to IPv6-only ::1, unreachable by common load tools)
- shelf_io_bench_server.dart: identical pipeline on shelf_io (port 8082)
- dart_io_bench_server.dart: raw dart:io ceiling, no shelf (port 8083)

Measured (AOT, CPU-pinned, ab -k -c 50): bottom_shelf ~53k RPS,
dart:io ~18k, shelf_io ~15.6k.
Doc refresh: 15 files / 655 lines -> 3 files / 377 lines, with all open
work tracked in one place.

- ROADMAP.md is now the single tracker: Phases 1-4 compressed to a
  completed summary; new phases capture the 2026-07-06 review findings:
  - Phase 5: correctness/security follow-ups (parser buffer-reuse data
    leak and response header injection marked CRITICAL)
  - Phase 6: performance work, ranked, with measure-first discipline
  - Phase 7: API & compliance hygiene (carried from TODO.md)
  - Phase 8: pkg:shelf upstream fast-path track with related issue links
- BENCHMARKS.md (new): measured results (bottom_shelf ~53k RPS, 3.4x
  shelf_io, 2.9x raw dart:io; AOT, CPU-pinned, interleaved trials),
  reproduction commands, and methodology rules
- PERFORMANCE_ANALYSIS.md: corrected overstated claims in place
  (stress_tester RPS artifact, "zero-allocation" parser, "100%"
  compatibility resting on shelf src/ imports)
- Deleted completed/stale docs: PHASE_*.md (8), THE_REST.md, TODO.md,
  perf_ideas.md, review_review.md, stress.md,
  test_complete_bottom_shelf.md
OPTIONAL changes: benchmarking (5 interleaved trials, AOT, CPU-pinned,
ab -k -c 50) shows NO measurable throughput difference vs baseline
(~51.5k vs ~51.3k RPS, inside the ±3% noise band). The only measured
effect is ~3% less garbage: 152 vs 157 GC scavenges over 300k identical
requests (reproducible across runs). Per-request allocation is not the
bottleneck at this level; keep or drop these on code-quality grounds.

- Skip the always-discarded first Uri.parse for origin-form request
  targets (the common case); non-origin-form requests take the exact
  old path
- Set TCP_NODELAY on accepted sockets (no effect on loopback benchmarks;
  matters for latency on real networks)
- Micro-fixes: byte-compare common methods instead of allocating a
  sublist view; const '1.1' fast path for HTTP/1.1 version; static
  identity function in TypedHeaders.host; shared empty Uint8List for
  bodiless requests; sync fast path around handler invocation; skip
  awaiting completed bodyDone; cache ErrorResponse.bytes; build
  _HttpConnectionInfo once per connection instead of per request
Profile-guided investigation (2026-07-07, idle machine, AOT, CPU-pinned,
interleaved trials): ~60% of CPU is socket syscalls; the addressable costs
are async/stream machinery and string/map churn, not the parser (1.9%).
Five independent prototypes measured: sync buffered-body path +10.2%,
byte-oriented serializer +7.3%, no per-response flush +5.6%, shelf Request
tax removed +4.4%, fused header scan +2.9%. Combined: +29.9% (53.2k -> 69.0k
RPS, 4.4x shelf_io) with 56% less GC. Effects are additive (predicted
+30.4% vs measured +29.9%).

- docs/PROFILE_2026_07.md: full report — CPU/allocation/syscall profiles,
  showdown results, per-prototype productionization notes and caveats
- docs/prototypes/: the six measurement patches + README (explicitly NOT
  production-ready; P1/P5 skip header validation, P3 drops backpressure,
  P4 removes load-bearing URL validation)
- docs/ROADMAP.md: Phase 6 re-ranked by measured data (parser rewrite
  demoted; landed items marked done with honest null results); Phase 8
  updated with measured +14.6% shelf-side receipts and the
  Request.adapter validation lesson (cf. #369)
- docs/BENCHMARKS.md: 2026-07-07 results section
- tool/vm_profile.dart: dependency-free vm_service profiler client
  (CPU samples + allocation profile) used for the investigation
Compute content-length count/validity/value, transfer-encoding presence,
chunked-ness, host + duplicate detection, and the Connection token in a
single walk of the header slices in the TypedHeaders constructor,
replacing ~8 separate walks and the per-request _cache map inserts.
validateTransferEncoding() now early-outs when no TE header is present.

Semantics preserved exactly: all Content-Length occurrences counted, every
TE header still individually validated, first recognized Connection token
wins (as before). Verified by the smuggling/robustness suites.

Measured +2.9% RPS (54,688 vs 53,154 median of 5 interleaved trials); see
docs/PROFILE_2026_07.md.
Rewrites RawShelfResponseSerializer to build response heads as bytes in a
reusable scratch buffer (materialized to the heap before any await, since
concurrent connections interleave at await boundaries):

- const status-line and Connection/Transfer-Encoding byte constants;
  Date header cached as bytes per second
- no StringBuffer -> toString -> utf8.encode round trip, no per-header
  toLowerCase()/join(), known headers matched by length + ASCII compare
- content-length captured during the existing headersAll iteration; no
  more Message.contentLength calls (each one hydrated shelf's entire
  singleValues map per response)

SECURITY: fixes response splitting / header injection. Header names must
be RFC 9110 tokens; values reject NUL, CR, LF, and non-Latin-1 code
units. Violations throw before any bytes reach the socket, so the
connection error path returns a clean 500 with no partial response on
the wire. Previously names/values were written verbatim.

Behavior change: header values above U+00FF were previously UTF-8
encoded on the wire (invalid per RFC 9110 field-value grammar); they are
now rejected with a 500.

Prototype without validation measured +7.3% RPS (57,024 vs 53,154; see
docs/PROFILE_2026_07.md).

Adds test/response_header_validation_test.dart (8 cases: CRLF/LF/NUL/
non-Latin-1/name-injection rejection, Latin-1 pass-through, multi-value
join).
HeaderByteSlices point into the parser's single reused buffer. reset()
runs right after the response is written, and the next keep-alive request
overwrites the buffer from index 0. A handler that retained the Request
past response completion (post-response logging, unawaited analytics) and
then lazily read an un-hydrated header would read ANOTHER request's bytes
— a silent cross-request data leak.

Fix by poisoning: a SliceBufferToken shared by all slices of one request
is invalidated in reset(); asString/matches/matchesKey then throw
StateError instead of reading the reused buffer. Chosen over
force-hydrating every request's headers on reset so the lazy-header fast
path (no string allocation for headers nobody reads) is preserved. The
danger window is exactly "first read after the response is sent", which
is never safe for a buffer-backed slice.

Adds test/slice_invalidation_test.dart. Throughput unchanged within noise
(the added validity check is one predictable branch).
The per-response `await socket.flush()` gated each pipelined keep-alive
request on the OS write draining. Drop it in favor of a bounded-buffer
policy: writeResponse now returns the byte count, the connection
accumulates it, and flushes only once $Limit.flushThreshold (256 KB) has
queued. This bounds a fast-handler/slow-client buffer to roughly the
threshold plus one response while letting small responses pipeline
without blocking.

Unflushed bytes are still delivered — dart:io drains socket.add
asynchronously without flush(); flush() only provides a completion
signal. The non-keep-alive and error paths still flush via socket.close()
/ _flushCloseDestroy(). Verified by a new 50-response keep-alive delivery
test in bottom_shelf_test.dart.

Measured +6% RPS (61,954 vs 58,419 median of 4 interleaved trials),
consistent with the +5.6% prototype in docs/PROFILE_2026_07.md.
Cumulative effect of the three landed perf commits (fused header scan,
byte-oriented serializer, byte-threshold flush) vs branch start e7e8621:
+16.2% RPS (52,881 -> 61,454 median) and -34% GC scavenges (157 -> 103),
pure bottom_shelf with no pkg:shelf changes.
kevmoo added 5 commits July 8, 2026 14:06
A client RST during the response flush/write raises an unhandled
SocketException (errno 104) that crashes the single isolate. Found via
the gcp-http-bench two-VM harness (wrk connection-count sweep sends an
RST when recycling its pool between steps); ab -k on loopback never hit
it. Roadmap item records the root cause (_flushCloseDestroy's unguarded
socket.flush().then, vs the guarded read path) and the fix direction
(catch ECONNRESET/EPIPE on write and destroy that connection).
…te path

- Catch and ignore errors on `socket.done` in `_HttpConnection` to prevent unhandled write exceptions from escaping.

- Guard `socket.flush()` and `socket.close()` in `_flushCloseDestroy` to avoid uncaught cleanup exceptions.

- Silently swallow `SocketException` on general catches and `_handleAsyncError` by destroying the connection.

- Guard `serverSocket.listen` and `_handleConnection` in `RawShelfServer` to handle early client disconnects during accept.

- Add regression test for client connection resets during response streaming.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type-infra A repository infrastructure change or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants