Introduce `routeNotFound` sentinel for shelf_router 1.1.0 (#141)
diff --git a/pkgs/shelf_router/CHANGELOG.md b/pkgs/shelf_router/CHANGELOG.md
index 1c9c9dd..0c7e1ac 100644
--- a/pkgs/shelf_router/CHANGELOG.md
+++ b/pkgs/shelf_router/CHANGELOG.md
@@ -1,3 +1,25 @@
+## v1.1.0
+ * `params` is deprecated in favor of `Request.params` adding using an extension
+ on `Request`.
+ * The default `notFoundHandler` now returns a sentinel `routeNotFound` response
+ object which causes 404 with the message 'Route not found'.
+ * __Minor breaking__: Handlers and sub-routers that return the sentinel
+ `routeNotFound` response object will be ignored and pattern matching will
+ continue on additional routes/handlers.
+
+Changing the router to continue pattern matching additional routes if a matched
+_handler_ or _nested router_ returns the sentinel `routeNotFound` response
+object is technically a _breaking change_. However, it only affects scenarios
+where the request matches a _mounted sub-router_, but does not match any route
+on this sub-router. In this case, `shelf_router` version `1.0.0` would
+immediately respond 404, without attempting to match further routes. With this
+release, the behavior changes to matching additional routes until one returns
+a custom 404 response object, or all routes have been matched.
+
+This behavior is more in line with how `shelf_router` version `0.7.x` worked,
+and since many affected users consider the behavior from `1.0.0` a defect,
+we decided to remedy the situation.
+
## v1.0.0
* Migrate package to null-safety
diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart
index 23eddab..5ddcd40 100644
--- a/pkgs/shelf_router/lib/src/router.dart
+++ b/pkgs/shelf_router/lib/src/router.dart
@@ -12,24 +12,50 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import 'dart:collection' show UnmodifiableMapView;
+
import 'package:http_methods/http_methods.dart';
import 'package:meta/meta.dart' show sealed;
import 'package:shelf/shelf.dart';
import 'package:shelf_router/src/router_entry.dart' show RouterEntry;
/// Get a URL parameter captured by the [Router].
+@Deprecated('Use Request.params instead')
String params(Request request, String name) {
- final p = request.context['shelf_router/params'];
- if (!(p is Map<String, String>)) {
- throw Exception('no such parameter $name');
- }
- final value = p[name];
+ final value = request.params[name];
if (value == null) {
throw Exception('no such parameter $name');
}
return value;
}
+final _emptyParams = UnmodifiableMapView(<String, String>{});
+
+extension RouterParams on Request {
+ /// Get URL parameters captured by the [Router].
+ ///
+ /// **Example**
+ /// ```dart
+ /// final app = Router();
+ ///
+ /// app.get('/hello/<name>', (Request request) {
+ /// final name = request.params['name'];
+ /// return Response.ok('Hello $name');
+ /// });
+ /// ```
+ ///
+ /// If no parameters are captured this returns an empty map.
+ ///
+ /// The returned map is unmodifiable.
+ Map<String, String> get params {
+ final p = context['shelf_router/params'];
+ if (p is Map<String, String>) {
+ return UnmodifiableMapView(p);
+ }
+ return _emptyParams;
+ }
+}
+
/// Middleware to remove body from request.
final _removeBody = createMiddleware(responseHandler: (r) {
if (r.headers.containsKey('content-length')) {
@@ -51,7 +77,7 @@
/// // Route pattern parameters can be specified <paramName>
/// app.get('/users/<userName>/whoami', (Request request) async {
/// // The matched values can be read with params(request, param)
-/// var userName = params(request, 'userName');
+/// var userName = request.params['userName'];
/// return Response.ok('You are ${userName}');
/// });
///
@@ -59,7 +85,8 @@
/// // doesn't implement Handler, it's assumed to take all parameters in the
/// // order they appear in the route pattern.
/// app.get('/users/<userName>/say-hello', (Request request, String userName) async {
-/// return Response.ok('Hello ${uName}');
+/// assert(userName == request.params['userName']);
+/// return Response.ok('Hello ${userName}');
/// });
///
/// // It is possible to have multiple parameters, and if desired a custom
@@ -67,7 +94,7 @@
/// // REGEXP is a regular expression (leaving out ^ and $).
/// // If no regular expression is specified `[^/]+` will be used.
/// app.get('/users/<userName>/messages/<msgId|\d+>', (Request request) async {
-/// var msgId = int.parse(params(request, 'msgId'));
+/// var msgId = int.parse(request.params['msgId']!);
/// return Response.ok(message.getById(msgId));
/// });
///
@@ -144,7 +171,10 @@
}
var params = route.match('/' + request.url.path);
if (params != null) {
- return await route.invoke(request, params);
+ final response = await route.invoke(request, params);
+ if (response != routeNotFound) {
+ return response;
+ }
}
}
return _notFoundHandler(request);
@@ -184,7 +214,58 @@
/// Handle `PATCH` request to [route] using [handler].
void patch(String route, Function handler) => add('PATCH', route, handler);
- static Response _defaultNotFound(Request request) {
- return Response.notFound('Not Found');
- }
+ static Response _defaultNotFound(Request request) => routeNotFound;
+
+ /// Sentinel [Response] object indicating that no matching route was found.
+ ///
+ /// This is the default response value from a [Router] created without a
+ /// `notFoundHandler`, when no routes matches the incoming request.
+ ///
+ /// If the [routeNotFound] object is returned from a [Handler] the [Router]
+ /// will consider the route _not matched_, and attempt to match other routes.
+ /// This is useful when mounting nested routers, or when matching a route
+ /// is conditioned on properties beyond the path of the URL.
+ ///
+ /// **Example**
+ /// ```dart
+ /// final app = Router();
+ ///
+ /// // The pattern for this route will match '/search' and '/search?q=...',
+ /// // but if request does not have `?q=...', then the handler will return
+ /// // [Router.routeNotFound] causing the router to attempt further routes.
+ /// app.get('/search', (Request request) async {
+ /// if (!request.uri.queryParameters.containsKey('q')) {
+ /// return Router.routeNotFound;
+ /// }
+ /// return Response.ok('TODO: make search results');
+ /// });
+ ///
+ /// // Same pattern as above
+ /// app.get('/search', (Request request) async {
+ /// return Response.ok('TODO: return search form');
+ /// });
+ ///
+ /// // Create a single nested router we can mount for handling API requests.
+ /// final api = Router();
+ ///
+ /// api.get('/version', (Request request) => Response.ok('1'));
+ ///
+ /// // Mounting router under '/api'
+ /// app.mount('/api', api);
+ ///
+ /// // If a request matches `/api/...` then the routes in the [api] router
+ /// // will be attempted. However, for a request like `/api/hello` there is
+ /// // no matching route in the [api] router. Thus, the router will return
+ /// // [Router.routeNotFound], which will cause matching to continue.
+ /// // Hence, the catch-all route below will be matched, causing a custom 404
+ /// // response with message 'nothing found'.
+ ///
+ /// // In the pattern below `<anything|.*>` is on the form `<name|regex>`,
+ /// // thus, this simply creates a URL parameter called `anything` which
+ /// // matches anything.
+ /// app.all('/<anything|.*>', (Request request) {
+ /// return Response.notFound('nothing found');
+ /// });
+ /// ```
+ static final Response routeNotFound = Response.notFound('Route not found');
}
diff --git a/pkgs/shelf_router/pubspec.yaml b/pkgs/shelf_router/pubspec.yaml
index 03f64df..f46854f 100644
--- a/pkgs/shelf_router/pubspec.yaml
+++ b/pkgs/shelf_router/pubspec.yaml
@@ -1,5 +1,5 @@
name: shelf_router
-version: 1.0.0
+version: 1.1.0
description: |
A convinent request router for the shelf web-framework, with support for
URL-parameters, nested routers and routers generated from source annotations.
diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart
index 01ca321..a05c292 100644
--- a/pkgs/shelf_router/test/router_test.dart
+++ b/pkgs/shelf_router/test/router_test.dart
@@ -74,8 +74,8 @@
var app = Router();
app.get(r'/user/<user>/groups/<group|\d+>', (Request request) {
- final user = params(request, 'user');
- final group = params(request, 'group');
+ final user = request.params['user'];
+ final group = request.params['group'];
return Response.ok('$user / $group');
});
@@ -111,14 +111,14 @@
app.mount('/api/', api);
app.all('/<_|[^]*>', (Request request) {
- return Response.notFound('catch-all-handler');
+ return Response.ok('catch-all-handler');
});
server.mount(app);
expect(await get('/hello'), 'hello-world');
expect(await get('/api/user/jonasfj/info'), 'Hello jonasfj');
- expect(get('/api/user/jonasfj/info-wrong'), throwsA(anything));
+ expect(await get('/api/user/jonasfj/info-wrong'), 'catch-all-handler');
});
test('mount(Handler) with middleware', () async {