bottom_shelf: cognitive-complexity and dedupe pass over the five hot functions (#545)

* refactor(bottom_shelf): decompose _HttpConnection._processData

Cognitive complexity 162 -> 29. The read loop keeps only flow control;
the per-request work moves into named steps with the same ordering and
the same destroy-vs-flush-then-close behaviour per rejection:

- _rejectMalformedHead / _rejectUnsupportedBody, via a _reject helper
  that replaces eleven copies of "socket.add(error); tear down; return"
- _parseRequestUri
- _attachBody, returning the body plus the bytes of the next request
- _startBodyTimer, _buildRequest, _hijack, _handleProcessingError

onPause/onResume closures become _pauseReads/_resumeReads tear-offs.

No test changes; 148 pass.

* refactor(bottom_shelf): flatten _executeRequest and share the error paths

Cognitive complexity: _executeRequest 35 -> 17, _handleAsyncError 19 -> <15,
start 16 -> <15.

- keep-alive tail uses early returns instead of nesting
- five copies of config.onConnectionError?.call(...) -> _reportError
- four copies of socket.close().then(destroy, onError: destroy)
  -> _closeThenDestroy; two of `await socket.close(); _destroy()`
  -> _closeAndDestroy
- onHeadersSent closure -> _markResponseSent tear-off
- socket listen callbacks become _onSocketError / _onSocketDone

No behaviour change; 148 tests pass.

* refactor(bottom_shelf): move RawHttpParser field completion out of the byte loop

Cognitive complexity of process() 121 -> 35. The per-byte validation stays
inline in the switch; the work done once per field (method, URL, version,
header key, header value, end of headers) moves to _finish* methods, with
_requireCrlf and _startField replacing three and five repeated fragments.
Version parsing becomes a single guard plus a switch.

Same exceptions in the same order; 148 tests pass.

* refactor(bottom_shelf): split ChunkedBodyController.add by state

Cognitive complexity 56 -> below threshold. Each state's byte handling is
its own method (_onSizeByte, _takeChunkData, _expectAfterChunkData,
_onTrailerByte); the loop only dispatches.

Removes _stateExt, which nothing ever entered: a ';' in the size line
throws notImplemented before the state could be reached. The int
_trailerState flag becomes bool _inTrailerLine.

148 tests pass.

* refactor(bottom_shelf): decompose RawShelfResponseSerializer.writeResponse

Cognitive complexity: writeResponse 64 -> below threshold, _writeStreamResponse
54 -> 18, with the header pass now in _addHeaders (27).

- _addHeaders serializes headers and returns (contentLength, isChunked)
- _addHeader / _addSetCookieHeaders / _addColonSpace
- _sendScratch replaces seven copies of "copy scratch, add, onHeadersSent"
- _writeBufferedBody holds the in-memory body cases
- _writeHeadersWithFirstChunk / _writeChunk hold the four coalescing
  variants of the streaming path; _chunkSizeLine, _contentLengthMismatch

Byte output is unchanged; 148 tests pass.

* perf(bottom_shelf): mark the extracted byte-loop helpers prefer-inline

The parser and chunked-body refactors moved once-per-field and
once-per-segment work out of their byte loops into small methods.
utils.dart already pins its byte classifiers with
@pragma('vm:prefer-inline') so AOT does not leave a call on the hot
path; do the same for these.

Measured with the bench_press micro suite (jit+aot, 66 cells): with the
pragmas in place every cell that moved is within the run-to-run
variance of two identical baseline runs (which themselves differ by up to
39% on two 256 KB / chunked cells and 3-27% on sixteen others).

* refactor(bottom_shelf): share the buffering base of the two body controllers

FixedLengthBodyController and ChunkedBodyController carried identical
copies of the pre-listen buffering: the StreamController setup with its
onListen replay, takeBufferedData, _addChunk, _close and addError (dedupe:
five clusters, 167 duplicate lines in this file). They move to an
abstract base _BufferingBodyController; each subclass keeps only its
framing state, isDone, add and its own close() semantics.

body_stream.dart 347 -> 287 lines. 148 tests pass.
4 files changed
tree: adae56df4ff5ae049302f7dcf49993777f5a3213
  1. .github/
  2. pkgs/
  3. tool/
  4. vendor/
  5. .gitignore
  6. .gitmodules
  7. analysis_options.yaml
  8. CONTRIBUTING.md
  9. LICENSE
  10. mono_repo.yaml
  11. README.md
README.md

Build Status

About Shelf

Shelf makes it easy to create and compose web servers and parts of web servers. How?

  • Expose a small set of simple types.
  • Map server logic into a simple function: a single argument for the request, the response is the return value.
  • Trivially mix and match synchronous and asynchronous processing.
  • Flexibility to return a simple string or a byte stream with the same model.

It was inspired by Connect for NodeJS and Rack for Ruby.

See the package:shelf readme for more information.

Packages

PackageDescriptionIssuesVersion
shelfA model for web server middleware that encourages composition and easy reuse.issuespub package
shelf_packages_handlerA shelf handler for serving a packages/ directory.issuespub package
shelf_proxyA shelf handler for proxying HTTP requests to another server.issuespub package
shelf_routerA convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.issuespub package
shelf_router_generatorA package:build-compatible builder for generating request routers for the shelf web-framework based on source annotations.issuespub package
shelf_staticStatic file server support for the shelf package and ecosystem.issuespub package
shelf_test_handlerA Shelf handler that makes it easy to test HTTP interactions.issuespub package
shelf_web_socketA shelf handler that wires up a listener for every connection.issuespub package
_shelf_complianceHTTP/1.1 compliance and hardening tests.issues(not published)

Publishing automation

For information about our publishing automation and release process, see https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.