CodeSampleX

Sample

shelf 1.4.2: Handle and test shelf HTTP requests in Dart without binding a port, by building a Request and calling the router, middleware and handler directly

Verified sample for pub shelf 1.4.2: Handle and test shelf HTTP requests in Dart without binding a port, by building a Request and calling the router…

sha256:2651162215727e626baac2f69d7b7e043707968fea0f9218aa9dda0648549e95

This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured. How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people. MIT-0

Execution evidence

The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.

Evidence basis
Signed contract pass
Verification receipts
2
Signing keys that built it
2
Declared environment dart 3 linux x64 dart 3 dart pub

Verification-run environments

Environment Contract Stages Run
dart 3 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · pub@1
2026-08-14
dart 3 · linux debian/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · pub@1
2026-08-18

Case

HOW
Goal
Handle and test shelf HTTP requests in Dart without binding a port, by building a Request and calling the router, middleware and handler directly
Packages
Symbols
  • Router
  • Router.routeNotFound
  • Request.params
  • Pipeline.addMiddleware
  • Middleware
  • Response.ok
  • Message.readAsString
  • Message.read
  • Response.change
  • Request.change
Environment
dart 3
Created
2026-08-14T14:13:26Z

Contract

  1. assert a constructed Request driven straight through the Router dispatches on method and path with no port bound, that an unregistered method on a matched path is a 404 and not a 405, that a <param> stops at a slash, and that registering GET also registers a HEAD in front of it whose body is stripped but whose content-length still reports the GET body length
  2. assert a handler taking only a Request receives the captures through the request.params extension getter, backed by a shelf_router/params context key that survives request.change, and that params outside a Router is an empty unmodifiable map rather than null
  3. assert a handler taking extra arguments is filled positionally in route-pattern order with the closure's parameter names ignored, and that a wrong argument count is accepted at registration and fails as a NoSuchMethodError on the first matching request
  4. assert the unmatched route returns Router.routeNotFound itself, one shared instance that can be read more than once, that returning it from a handler resumes matching while an identical-looking Response.notFound ends the request, and that notFoundHandler replaces it
  5. assert Pipeline runs middleware in declaration order inbound and the reverse outbound, proven by a response header each middleware appends to and by one they overwrite where the outermost writer wins, and that middleware answering without calling the inner handler cuts out everything declared below it
  6. assert a shelf body is single-subscription whether it was passed as a String or as a Stream, that the second read throws StateError synchronously out of readAsString, and that change() shares the drained Body so only change(body: ...) restores it
  7. assert a logging middleware that reads the response body forwards an unreadable response, that the request-side version breaks the route handler through the router's own request.change, and that both are fixed by putting the read body back
  8. assert header lookup is case-insensitive on requests and responses while the stored keys keep the case they were written with, and that a request served over loopback arrives with lowercase keys because dart:io lowercases them off the wire

Files

  • csx.json
  • lib/api.dart
  • pubspec.lock
  • pubspec.yaml
  • test/contract_test.dart

Download the source artifact (tar.gz)

Source

csx.json
{"case":{"caseId":"case:sha256:c27c1ac7a36f97e2258b1b133024cba0f673ce0c08d86faee861d788b2d1ae5d","constraints":{"runtime":"dart"},"contract":["assert a constructed Request driven straight through the Router dispatches on method and path with no port bound, that an unregistered method on a matched path is a 404 and not a 405, that a \u003cparam\u003e stops at a slash, and that registering GET also registers a HEAD in front of it whose body is stripped but whose content-length still reports the GET body length","assert a handler taking only a Request receives the captures through the request.params extension getter, backed by a shelf_router/params context key that survives request.change, and that params outside a Router is an empty unmodifiable map rather than null","assert a handler taking extra arguments is filled positionally in route-pattern order with the closure's parameter names ignored, and that a wrong argument count is accepted at registration and fails as a NoSuchMethodError on the first matching request","assert the unmatched route returns Router.routeNotFound itself, one shared instance that can be read more than once, that returning it from a handler resumes matching while an identical-looking Response.notFound ends the request, and that notFoundHandler replaces it","assert Pipeline runs middleware in declaration order inbound and the reverse outbound, proven by a response header each middleware appends to and by one they overwrite where the outermost writer wins, and that middleware answering without calling the inner handler cuts out everything declared below it","assert a shelf body is single-subscription whether it was passed as a String or as a Stream, that the second read throws StateError synchronously out of readAsString, and that change() shares the drained Body so only change(body: ...) restores it","assert a logging middleware that reads the response body forwards an unreadable response, that the request-side version breaks the route handler through the router's own request.change, and that both are fixed by putting the read body back","assert header lookup is case-insensitive on requests and responses while the stored keys keep the case they were written with, and that a request served over loopback arrives with lowercase keys because dart:io lowercases them off the wire"],"goal":"Handle and test shelf HTTP requests in Dart without binding a port, by building a Request and calling the router, middleware and handler directly","kind":"HOW","packages":["pkg:pub/shelf@1.4.2","pkg:pub/shelf_router@1.1.4"],"schemaVersion":1,"symbols":["Router","Router.routeNotFound","Request.params","Pipeline.addMiddleware","Middleware","Response.ok","Message.readAsString","Message.read","Response.change","Request.change"]},"contractCommand":["dart","test","--reporter=expanded"],"environment":{"arch":"x64","ecosystem":"pub","executionContext":"dart","language":"dart","os":"linux","packageManager":"pub","runtime":"dart","runtimeVersion":"3","schemaVersion":1},"license":"MIT-0","packages":["pkg:pub/shelf@1.4.2","pkg:pub/shelf_router@1.1.4"],"schemaVersion":1,"symbols":["Router","Router.routeNotFound","Request.params","Pipeline.addMiddleware","Middleware","Response.ok","Message.readAsString","Message.read","Response.change","Request.change"],"verifierAdapter":"pub@1"}
lib/api.dart
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

/// Handling shelf requests without binding a port. A `Handler` is nothing but
/// `FutureOr<Response> Function(Request)`, so a test builds a Request and calls
/// it. No socket, no mock, no fixture server: the router, the middleware and
/// the handler under test are all the real ones, and the whole round trip is a
/// function call.
///
/// Six things behave differently than the shape of the API suggests.
///
/// 1. shelf_router decides how to deliver URL parameters from the handler's
///    signature, not from the route. RouterEntry tests `_handler is Handler`
///    first: a handler that takes only a Request is called with only the
///    Request, and the captured values arrive through `request.params`, the
///    extension getter shelf_router adds to Request. A handler that takes extra
///    arguments has them applied positionally, in the order the parameters
///    appear in the route pattern — the closure's own parameter names are never
///    read, so swapping two of them silently swaps two values. Getting the
///    count wrong is not rejected when the route is registered; it is a
///    NoSuchMethodError on the first matching request.
///
/// 2. The default 404 is `Router.routeNotFound`, one shared object rather than
///    a fresh Response per request. shelf_router had to override `read()` on it
///    so the same instance can be served repeatedly, which is the loudest hint
///    in either package that an ordinary Response cannot. That object also
///    means "no match, keep looking" when a handler returns it: the router
///    resumes matching later routes. `Response.notFound('Route not found')` —
///    same status, same bytes — does not, because the router compares by
///    identity.
///
/// 3. `Pipeline().addMiddleware(a).addMiddleware(b).addHandler(h)` composes to
///    `a(b(h))`. Declaration order is the order requests are seen and the
///    reverse of the order responses are seen, so the outermost middleware
///    writes its response header last and wins a collision.
///
/// 4. Every shelf body is a single-subscription stream, whatever you passed in.
///    A String body is not stored as a String; `Body` encodes it to bytes and
///    wraps it in a Stream, and `read()` hands that stream over and nulls its
///    own reference. The second `readAsString()` is a StateError, thrown
///    synchronously out of the call rather than delivered to the Future. Worse
///    for middleware: `change()` carries the same Body object across, so a
///    logging middleware that reads the body and returns `response.change(...)`
///    forwards a response whose body is already gone. Read once, then put what
///    you read back with `change(body: ...)`.
///
/// 5. `Router.get` quietly registers two routes: the GET, and a HEAD in front
///    of it wrapped in a body-stripping middleware. A `router.head` you add
///    afterwards for the same pattern is dead code, because the generated one
///    already matched.
///
/// 6. Header lookup is case-insensitive, but the keys are not lowercased —
///    `CaseInsensitiveMap` canonicalises for lookup and keeps the spelling you
///    stored. dart:io lowercases header names as it parses them off the wire,
///    so a served request really does have lowercase keys, and a Request you
///    constructed has whatever case you typed. Only code that iterates `.keys`
///    or serialises the header map can tell, and that code passes in a unit
///    test and fails in production.

/// The application under test. Routes are registered against the real Router,
/// so what the test exercises is the routing, not a description of it.
Router buildApp() {
  final app = Router();

  // One argument, so shelf_router treats this as a plain Handler and the
  // capture arrives in request.params.
  app.get('/users/<id>', (Request request) {
    return Response.ok('user ${request.params['id']}');
  });

  // Extra arguments, so the captures are applied positionally instead. Both
  // styles are supported at once; the router chooses per route.
  app.get('/orgs/<org>/users/<id>', (Request request, String org, String id) {
    return Response.ok('$org/$id');
  });

  app.post('/users', (Request request) async {
    return Response(201, body: 'created ${await request.readAsString()}');
  });

  // Two handlers on one pattern. The first declines by returning the sentinel,
  // which sends the router back to matching rather than ending the request.
  app.get('/search', (Request request) {
    final q = request.url.queryParameters['q'];
    if (q == null) return Router.routeNotFound;
    return Response.ok('results for $q');
  });
  app.get('/search', (Request request) => Response.ok('search form'));

  return app;
}

/// Builds the Request an adapter would have built, without the socket. The
/// host is only there because Request requires an absolute requestedUri; the
/// router matches on `request.url`, which is the path relative to handlerPath.
Request buildRequest(
  String method,
  String path, {
  Map<String, Object>? headers,
  Object? body,
}) =>
    Request(
      method,
      Uri.parse('http://example.com$path'),
      headers: headers,
      body: body,
    );

/// Records the order it is entered and left, and appends its name to a shared
/// response header on the way out so the ordering can be read off the response
/// as well as off the log.
Middleware stamp(String name, List<String> log) =>
    (Handler innerHandler) => (Request request) async {
          log.add('$name >');
          final response = await innerHandler(request);
          log.add('< $name');
          final existing = response.headers['x-stamp'];
          return response.change(headers: {
            'x-stamp': existing == null ? name : '$existing,$name',
          });
        };

/// Sets a header outright instead of appending to it, so a pipeline of these
/// shows which writer lands last rather than only in which order they ran.
Middleware setOwner(String name) =>
    (Handler innerHandler) => (Request request) async =>
        (await innerHandler(request)).change(headers: {'x-owner': name});

/// Answers without calling the inner handler, which is what makes middleware
/// order load-bearing rather than cosmetic.
Middleware requireToken() => (Handler innerHandler) => (Request request) async {
      if (request.headers['authorization'] != 'Bearer t0ken') {
        return Response.forbidden('no token');
      }
      return innerHandler(request);
    };

/// The logging middleware everybody writes first. It reads the body to log it
/// and hands the same Response on, and `change` brings the drained Body with
/// it, so the adapter finds nothing left to write.
Middleware drainingLogger(List<String> log) =>
    (Handler innerHandler) => (Request request) async {
          final response = await innerHandler(request);
          log.add(await response.readAsString());
          return response.change(headers: {'x-logged': 'yes'});
        };

/// The same middleware with the one fix: put back what you took.
Middleware bodyLogger(List<String> log) =>
    (Handler innerHandler) => (Request request) async {
          final response = await innerHandler(request);
          final body = await response.readAsString();
          log.add(body);
          return response.change(body: body, headers: {'x-logged': 'yes'});
        };

/// The request-side version of the same mistake. The handler downstream gets a
/// Request whose body has already been consumed.
Middleware drainingRequestLogger(List<String> log) =>
    (Handler innerHandler) => (Request request) async {
          log.add(await request.readAsString());
          return innerHandler(request);
        };

/// And its fix, for the request side.
Middleware requestLogger(List<String> log) =>
    (Handler innerHandler) => (Request request) async {
          final body = await request.readAsString();
          log.add(body);
          return innerHandler(request.change(body: body));
        };
pubspec.lock
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
  _fe_analyzer_shared:
    dependency: transitive
    description:
      name: _fe_analyzer_shared
      sha256: "9a3386eea899815698dd55995277cf7cb8572ee52b399a6edfb7ae2b50e5fc19"
      url: "https://pub.dev"
    source: hosted
    version: "105.0.0"
  analyzer:
    dependency: transitive
    description:
      name: analyzer
      sha256: "62993bed6eadbe9596c5c20d5c167e7bc563c5fe266657a04ddeb93bdb84f4c9"
      url: "https://pub.dev"
    source: hosted
    version: "14.1.0"
  args:
    dependency: transitive
    description:
      name: args
      sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
      url: "https://pub.dev"
    source: hosted
    version: "2.7.0"
  async:
    dependency: transitive
    description:
      name: async
      sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
      url: "https://pub.dev"
    source: hosted
    version: "2.13.1"
  boolean_selector:
    dependency: transitive
    description:
      name: boolean_selector
      sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
      url: "https://pub.dev"
    source: hosted
    version: "2.1.2"
  cli_config:
    dependency: transitive
    description:
      name: cli_config
      sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
      url: "https://pub.dev"
    source: hosted
    version: "0.2.0"
  collection:
    dependency: transitive
    description:
      name: collection
      sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
      url: "https://pub.dev"
    source: hosted
    version: "1.19.1"
  convert:
    dependency: transitive
    description:
      name: convert
      sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
      url: "https://pub.dev"
    source: hosted
    version: "3.1.2"
  coverage:
    dependency: transitive
    description:
      name: coverage
      sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
      url: "https://pub.dev"
    source: hosted
    version: "1.15.1"
  crypto:
    dependency: transitive
    description:
      name: crypto
      sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
      url: "https://pub.dev"
    source: hosted
    version: "3.0.7"
  file:
    dependency: transitive
    description:
      name: file
      sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
      url: "https://pub.dev"
    source: hosted
    version: "7.0.1"
  frontend_server_client:
    dependency: transitive
    description:
      name: frontend_server_client
      sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
      url: "https://pub.dev"
    source: hosted
    version: "4.0.0"
  glob:
    dependency: transitive
    description:
      name: glob
      sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
      url: "https://pub.dev"
    source: hosted
    version: "2.1.3"
  http_methods:
    dependency: transitive
    description:
      name: http_methods
      sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
      url: "https://pub.dev"
    source: hosted
    version: "1.1.1"
  http_multi_server:
    dependency: transitive
    description:
      name: http_multi_server
      sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
      url: "https://pub.dev"
    source: hosted
    version: "3.2.2"
  http_parser:
    dependency: transitive
    description:
      name: http_parser
      sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
      url: "https://pub.dev"
    source: hosted
    version: "4.1.2"
  io:
    dependency: transitive
    description:
      name: io
      sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
      url: "https://pub.dev"
    source: hosted
    version: "1.0.5"
  logging:
    dependency: transitive
    description:
      name: logging
      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
      url: "https://pub.dev"
    source: hosted
    version: "1.3.0"
  matcher:
    dependency: transitive
    description:
      name: matcher
      sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
      url: "https://pub.dev"
    source: hosted
    version: "0.12.20"
  meta:
    dependency: transitive
    description:
      name: meta
      sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
      url: "https://pub.dev"
    source: hosted
    version: "1.19.0"
  mime:
    dependency: transitive
    description:
      name: mime
      sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
      url: "https://pub.dev"
    source: hosted
    version: "2.0.0"
  node_preamble:
    dependency: transitive
    description:
      name: node_preamble
      sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
      url: "https://pub.dev"
    source: hosted
    version: "2.0.2"
  package_config:
    dependency: transitive
    description:
      name: package_config
      sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
      url: "https://pub.dev"
    source: hosted
    version: "3.0.0"
  path:
    dependency: transitive
    description:
      name: path
      sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
      url: "https://pub.dev"
    source: hosted
    version: "1.9.1"
  pool:
    dependency: transitive
    description:
      name: pool
      sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
      url: "https://pub.dev"
    source: hosted
    version: "1.5.2"
  pub_semver:
    dependency: transitive
    description:
      name: pub_semver
      sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
      url: "https://pub.dev"
    source: hosted
    version: "2.2.0"
  shelf:
    dependency: "direct main"
    description:
      name: shelf
      sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
      url: "https://pub.dev"
    source: hosted
    version: "1.4.2"
  shelf_packages_handler:
    dependency: transitive
    description:
      name: shelf_packages_handler
      sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
      url: "https://pub.dev"
    source: hosted
    version: "3.0.2"
  shelf_router:
    dependency: "direct main"
    description:
      name: shelf_router
      sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864
      url: "https://pub.dev"
    source: hosted
    version: "1.1.4"
  shelf_static:
    dependency: transitive
    description:
      name: shelf_static
      sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
      url: "https://pub.dev"
    source: hosted
    version: "1.1.3"
  shelf_web_socket:
    dependency: transitive
    description:
      name: shelf_web_socket
      sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
      url: "https://pub.dev"
    source: hosted
    version: "3.0.0"
  source_map_stack_trace:
    dependency: transitive
    description:
      name: source_map_stack_trace
      sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
      url: "https://pub.dev"
    source: hosted
    version: "2.1.2"
  source_maps:
    dependency: transitive
    description:
      name: source_maps
      sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
      url: "https://pub.dev"
    source: hosted
    version: "0.10.13"
  source_span:
    dependency: transitive
    description:
      name: source_span
      sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
      url: "https://pub.dev"
    source: hosted
    version: "1.10.2"
  stack_trace:
    dependency: transitive
    description:
      name: stack_trace
      sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
      url: "https://pub.dev"
    source: hosted
    version: "1.12.1"
  stream_channel:
    dependency: transitive
    description:
      name: stream_channel
      sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
      url: "https://pub.dev"
    source: hosted
    version: "2.1.4"
  string_scanner:
    dependency: transitive
    description:
      name: string_scanner
      sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
      url: "https://pub.dev"
    source: hosted
    version: "1.4.1"
  term_glyph:
    dependency: transitive
    description:
      name: term_glyph
      sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
      url: "https://pub.dev"
    source: hosted
    version: "1.2.2"
  test:
    dependency: "direct dev"
    description:
      name: test
      sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
      url: "https://pub.dev"
    source: hosted
    version: "1.31.2"
  test_api:
    dependency: transitive
    description:
      name: test_api
      sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
      url: "https://pub.dev"
    source: hosted
    version: "0.7.13"
  test_core:
    dependency: transitive
    description:
      name: test_core
      sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
      url: "https://pub.dev"
    source: hosted
    version: "0.6.19"
  typed_data:
    dependency: transitive
    description:
      name: typed_data
      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
      url: "https://pub.dev"
    source: hosted
    version: "1.4.0"
  vm_service:
    dependency: transitive
    description:
      name: vm_service
      sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
      url: "https://pub.dev"
    source: hosted
    version: "15.2.0"
  watcher:
    dependency: transitive
    description:
      name: watcher
      sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
      url: "https://pub.dev"
    source: hosted
    version: "1.2.1"
  web:
    dependency: transitive
    description:
      name: web
      sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
      url: "https://pub.dev"
    source: hosted
    version: "1.1.1"
  web_socket:
    dependency: transitive
    description:
      name: web_socket
      sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
      url: "https://pub.dev"
    source: hosted
    version: "1.0.1"
  web_socket_channel:
    dependency: transitive
    description:
      name: web_socket_channel
      sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
      url: "https://pub.dev"
    source: hosted
    version: "3.0.3"
  webkit_inspection_protocol:
    dependency: transitive
    description:
      name: webkit_inspection_protocol
      sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
      url: "https://pub.dev"
    source: hosted
    version: "1.2.1"
  yaml:
    dependency: transitive
    description:
      name: yaml
      sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
      url: "https://pub.dev"
    source: hosted
    version: "3.1.3"
sdks:
  dart: ">=3.11.0 <4.0.0"
pubspec.yaml
name: csx_shelf_requests
description: Handling shelf requests by calling the handler directly, with no port bound.
publish_to: none
environment:
  sdk: ^3.5.0
dependencies:
  shelf: 1.4.2
  shelf_router: 1.1.4
dev_dependencies:
  test: 1.31.2
test/contract_test.dart
import 'dart:async';
import 'dart:io';

import 'package:csx_shelf_requests/api.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';
import 'package:test/test.dart';

void main() {
  test('a Router dispatches on method and path, with no port bound', () async {
    final app = buildApp();

    // The whole round trip: build a Request, call the Router, read the
    // Response. Router implements call(), so it is itself a Handler.
    final user = await app(buildRequest('GET', '/users/u42'));
    expect(user.statusCode, equals(200));
    expect(await user.readAsString(), equals('user u42'));

    final created = await app(buildRequest('POST', '/users', body: 'ada'));
    expect(created.statusCode, equals(201));
    expect(await created.readAsString(), equals('created ada'));

    // Same path, unregistered method. shelf_router does not do 405: a method
    // it has no route for is indistinguishable from a path it has no route
    // for, so this is the plain 404.
    expect((await app(buildRequest('DELETE', '/users/u42'))).statusCode,
        equals(404));

    // A GET route answers HEAD too, because Router.add registers a second
    // entry for it wrapped in a body-stripping middleware. The status matches
    // the GET and the body is gone.
    final head = await app(buildRequest('HEAD', '/users/u42'));
    expect(head.statusCode, equals(200));
    expect(await head.readAsString(), isEmpty);
    // Measured, against the expectation that stripping the body zeroes the
    // length: it is still the length of the GET body. The stripping middleware
    // sets content-length to '0' first and Response.change recomputes the
    // header from the body it carries over, undoing that; the later
    // change(body: []) then leaves the header alone. RFC 9110 wants exactly
    // this number on a HEAD, but nothing in the code aimed for it, so assert
    // the number rather than the intent.
    expect(head.headers['content-length'], equals('8'));
    expect((await app(buildRequest('GET', '/users/u42'))).contentLength,
        equals(8));

    // <id> compiles to [^/]+, so it stops at a slash and does not swallow the
    // rest of the path.
    expect((await app(buildRequest('GET', '/users/u42/roles'))).statusCode,
        equals(404));

    // The generated HEAD entry is registered ahead of the GET, so a head route
    // added afterwards for the same pattern never matches. Registering it
    // first is the documented way to own HEAD, and it works.
    final headLast = Router()
      ..get('/ping', (Request request) => Response.ok('pong'))
      ..head('/ping',
          (Request request) => Response.ok('', headers: {'x-from': 'head'}));
    expect((await headLast(buildRequest('HEAD', '/ping'))).headers['x-from'],
        isNull);

    final headFirst = Router()
      ..head('/ping',
          (Request request) => Response.ok('', headers: {'x-from': 'head'}))
      ..get('/ping', (Request request) => Response.ok('pong'));
    expect((await headFirst(buildRequest('HEAD', '/ping'))).headers['x-from'],
        equals('head'));
  });

  test('captures reach a one-argument handler through request.params',
      () async {
    final app = buildApp();

    expect(await (await app(buildRequest('GET', '/users/u42'))).readAsString(),
        equals('user u42'));

    // Outside a Router the extension getter is not an error and not null: it
    // is an empty unmodifiable map, so a route registered without <id> reads
    // params['id'] as null rather than failing.
    final bare = buildRequest('GET', '/users/u42');
    expect(bare.params, isEmpty);
    expect(bare.params['id'], isNull);
    expect(() => bare.params['id'] = 'u42', throwsUnsupportedError);

    // params is carried in the request context under a namespaced key, which
    // is how it survives request.change() through middleware.
    late Request seen;
    final router = Router()
      ..get('/users/<id>', (Request request) {
        seen = request;
        return Response.ok('');
      });
    await router(buildRequest('GET', '/users/u42'));
    expect(seen.context['shelf_router/params'], equals({'id': 'u42'}));
    // change() copies the context, so an inner middleware that rewrites the
    // request keeps the captures the router attached.
    expect(
        seen.change(headers: {'x-inner': '1'}).params, equals({'id': 'u42'}));
  });

  test('a multi-argument handler is filled by position, never by name',
      () async {
    final app = buildApp();
    expect(
      await (await app(buildRequest('GET', '/orgs/acme/users/u42')))
          .readAsString(),
      equals('acme/u42'),
    );

    // The names in the closure are decoration. This handler calls the first
    // argument `id` and the second `org`, and still receives them in the order
    // the pattern captures them, so the values come out swapped.
    final swapped = Router()
      ..get('/orgs/<org>/users/<id>',
          (Request request, String id, String org) => Response.ok('$id/$org'));
    expect(
      await (await swapped(buildRequest('GET', '/orgs/acme/users/u42')))
          .readAsString(),
      equals('acme/u42'),
    );

    // Arity is not checked when the route is registered. Registering succeeds
    // and the failure arrives on the first matching request, as a
    // NoSuchMethodError from Function.apply.
    final wrongArity = Router()
      ..get('/users/<id>',
          (Request request, String id, String extra) => Response.ok(''));
    await expectLater(
      wrongArity(buildRequest('GET', '/users/u42')),
      throwsA(isA<NoSuchMethodError>()),
    );
  });

  test(
      'the unmatched route returns the router 404, a shared re-readable object',
      () async {
    final app = buildApp();

    final missing = await app(buildRequest('GET', '/nope'));
    expect(missing.statusCode, equals(404));
    expect(await missing.readAsString(), equals('Route not found'));

    // It is not a response built for this request: the default notFoundHandler
    // returns the one static instance every Router shares, and shelf_router had
    // to override read() on it so serving it twice works. That override is why
    // reading it twice below succeeds where a Response you built would throw.
    expect(identical(missing, Router.routeNotFound), isTrue);
    expect(await missing.readAsString(), equals('Route not found'));

    // Returning that object from a handler means "not matched, keep going",
    // so /search without ?q falls through to the second handler registered on
    // the same pattern.
    expect(await (await app(buildRequest('GET', '/search'))).readAsString(),
        equals('search form'));
    expect(
        await (await app(buildRequest('GET', '/search?q=shelf')))
            .readAsString(),
        equals('results for shelf'));

    // A look-alike does not fall through. Same status, same body text, but the
    // router compares by identity, so this ends the request and the second
    // handler is never reached.
    final lookalike = Router()
      ..get(
          '/search', (Request request) => Response.notFound('Route not found'))
      ..get('/search', (Request request) => Response.ok('search form'));
    final ended = await lookalike(buildRequest('GET', '/search'));
    expect(ended.statusCode, equals(404));
    expect(identical(ended, Router.routeNotFound), isFalse);

    // The 404 is a handler like any other and can be replaced.
    final custom = Router(
        notFoundHandler: (Request request) =>
            Response.notFound('no route for ${request.url.path}'));
    expect(await (await custom(buildRequest('GET', '/nope'))).readAsString(),
        equals('no route for nope'));
  });

  test('Pipeline runs middleware in declaration order and unwinds in reverse',
      () async {
    final log = <String>[];
    final handler = const Pipeline()
        .addMiddleware(stamp('outer', log))
        .addMiddleware(stamp('inner', log))
        .addHandler((Request request) {
      log.add('handler');
      return Response.ok('body');
    });

    final response = await handler(buildRequest('GET', '/'));

    // addMiddleware(a).addMiddleware(b) composes to a(b(handler)), so requests
    // travel outward-in and responses inward-out.
    expect(
        log, equals(['outer >', 'inner >', 'handler', '< inner', '< outer']));

    // Read off the response instead of off the log: inner appended first, so
    // the outermost middleware is the one whose value ends up last.
    expect(response.headers['x-stamp'], equals('inner,outer'));

    // Appending hides which write lands last, so run it again with middleware
    // that overwrite. The outermost one writes on the way out, after the
    // handler and after everything nested inside it, so its value is the one
    // that reaches the client.
    final collided = const Pipeline()
        .addMiddleware(setOwner('outer'))
        .addMiddleware(setOwner('inner'))
        .addHandler((Request request) =>
            Response.ok('body', headers: {'x-owner': 'handler'}));
    expect((await collided(buildRequest('GET', '/'))).headers['x-owner'],
        equals('outer'));

    // Middleware that answers without calling the inner handler cuts
    // everything below it out of the request, including the middleware
    // declared after it.
    log.clear();
    final guarded = const Pipeline()
        .addMiddleware(stamp('outer', log))
        .addMiddleware(requireToken())
        .addMiddleware(stamp('inner', log))
        .addHandler((Request request) {
      log.add('handler');
      return Response.ok('body');
    });

    final rejected = await guarded(buildRequest('GET', '/'));
    expect(rejected.statusCode, equals(403));
    expect(log, equals(['outer >', '< outer']));
    expect(rejected.headers['x-stamp'], equals('outer'));

    final allowed = await guarded(
        buildRequest('GET', '/', headers: {'authorization': 'Bearer t0ken'}));
    expect(allowed.statusCode, equals(200));
    expect(log.last, equals('< outer'));
    expect(allowed.headers['x-stamp'], equals('inner,outer'));
  });

  test('a body is a single-subscription stream whatever type you passed',
      () async {
    // Measured, against the expectation that a String body is held as a String
    // and can be read as often as you like. Body encodes it once and keeps a
    // Stream; there is no stored String to re-read, so the String case and the
    // Stream case fail identically.
    for (final response in [
      Response.ok('hello'),
      Response.ok(Stream<List<int>>.value('hello'.codeUnits)),
    ]) {
      expect(await response.readAsString(), equals('hello'));
      // The StateError is thrown out of the call, not delivered to the Future:
      // read() runs before readAsString builds one. `await` still catches it,
      // but .catchError on the returned Future never fires.
      expect(
        () => response.readAsString(),
        throwsA(isA<StateError>().having(
          (e) => e.message,
          'message',
          "The 'read' method can only be called once on a "
              'shelf.Request/shelf.Response object.',
        )),
      );
      expect(() => response.read(), throwsStateError);
    }

    // Requests are the same object model, so the same rule applies.
    final request = buildRequest('POST', '/users', body: 'ada');
    expect(await request.readAsString(), equals('ada'));
    expect(() => request.readAsString(), throwsStateError);

    // change() copies headers and context but shares the Body instance, which
    // is what turns "I only logged it" into an empty response.
    final drained = Response.ok('hello');
    await drained.readAsString();
    expect(() => drained.change(headers: {'x-logged': 'yes'}).readAsString(),
        throwsStateError);

    // Putting the string back is the whole fix, and it is a new Body.
    final refilled = Response.ok('hello');
    final captured = await refilled.readAsString();
    final forwarded = refilled.change(body: captured);
    expect(await forwarded.readAsString(), equals('hello'));
    expect(forwarded.headers['content-length'], equals('5'));
  });

  test('reading the body in middleware breaks the handler below it', () async {
    final log = <String>[];

    // Response side: the middleware logs the body and the caller gets nothing.
    final broken = const Pipeline()
        .addMiddleware(drainingLogger(log))
        .addHandler((Request request) => Response.ok('hello'));
    final emptied = await broken(buildRequest('GET', '/'));
    expect(log, equals(['hello']));
    expect(emptied.headers['x-logged'], equals('yes'));
    expect(() => emptied.readAsString(), throwsStateError);

    log.clear();
    final fixed = const Pipeline()
        .addMiddleware(bodyLogger(log))
        .addHandler((Request request) => Response.ok('hello'));
    final intact = await fixed(buildRequest('GET', '/'));
    expect(log, equals(['hello']));
    expect(intact.headers['x-logged'], equals('yes'));
    expect(await intact.readAsString(), equals('hello'));

    // Request side, through the real Router: shelf_router calls
    // request.change() to attach params, which carries the drained Body along,
    // so the route handler's readAsString fails.
    log.clear();
    final brokenIn = const Pipeline()
        .addMiddleware(drainingRequestLogger(log))
        .addHandler(buildApp().call);
    await expectLater(
      brokenIn(buildRequest('POST', '/users', body: 'ada')),
      throwsA(isA<StateError>()),
    );
    expect(log, equals(['ada']));

    log.clear();
    final fixedIn = const Pipeline()
        .addMiddleware(requestLogger(log))
        .addHandler(buildApp().call);
    final ok = await fixedIn(buildRequest('POST', '/users', body: 'ada'));
    expect(log, equals(['ada']));
    expect(await ok.readAsString(), equals('created ada'));
  });

  test('header lookup ignores case; the stored key keeps the case you wrote',
      () async {
    final request = buildRequest(
      'POST',
      '/users',
      headers: {'Content-Type': 'application/json', 'X-Request-Id': 'abc'},
      body: '{"n":1}',
    );

    // Lookup is canonicalised, so every spelling finds the value and
    // containsKey agrees.
    for (final spelling in ['Content-Type', 'content-type', 'CONTENT-TYPE']) {
      expect(request.headers[spelling], equals('application/json'));
      expect(request.headers.containsKey(spelling), isTrue);
    }
    expect(request.mimeType, equals('application/json'));
    expect(request.headers['x-request-id'], equals('abc'));

    // Measured, against the expectation that shelf lowercases header names on
    // the way in. CaseInsensitiveMap canonicalises for lookup only and hands
    // back the key as stored, so a constructed Request keeps your capitals.
    // Only iteration can tell, which is exactly what a header-forwarding proxy
    // or a snapshot assertion does.
    expect(request.headers.keys, contains('Content-Type'));
    expect(request.headers.keys, isNot(contains('content-type')));
    // shelf added this one itself, in the spelling shelf uses.
    expect(request.headers['content-length'], equals('7'));
    expect(request.headers.keys, contains('content-length'));

    // Responses behave the same way, and headersAll is the multi-value view of
    // the identical map.
    final response = Response.ok('hi', headers: {
      'X-Trace': ['a', 'b']
    });
    expect(response.headers['x-trace'], equals('a,b'));
    expect(response.headersAll['x-trace'], equals(['a', 'b']));
    expect(response.headers.keys, contains('X-Trace'));

    // Where the lowercase belief comes from: dart:io lowercases header names
    // while parsing them, so a request that arrived over a socket really does
    // have lowercase keys. Served on loopback, which works with the network
    // disabled, with the mixed-case name written straight onto the wire so
    // nothing in a client library can be blamed for the change.
    final served = Completer<List<String>>();
    final server = await shelf_io.serve((Request request) {
      if (!served.isCompleted) served.complete(request.headers.keys.toList());
      return Response.ok('');
    }, InternetAddress.loopbackIPv4, 0);
    final socket =
        await Socket.connect(InternetAddress.loopbackIPv4, server.port);
    socket.write('GET /users/u42 HTTP/1.1\r\n'
        'Host: example.com\r\n'
        'X-Request-Id: abc\r\n'
        'Connection: close\r\n'
        '\r\n');
    await socket.flush();
    final servedKeys = await served.future;
    socket.destroy();
    await server.close(force: true);

    expect(servedKeys, contains('x-request-id'));
    expect(servedKeys, isNot(contains('X-Request-Id')));
  });
}

Origin Seeder

anonymous