@@ -36,8 +36,14 @@ class Router {
36
36
///
37
37
/// The [notFoundHandler] will be invoked for requests where no matching route
38
38
/// was found. By default, a simple 404 response will be used.
39
- Router ({Handler notFoundHandler = _defaultNotFound})
40
- : _notFoundHandler = notFoundHandler;
39
+ ///
40
+ /// The [methodNotAllowedHandler] will be invoked for requests where the HTTP
41
+ /// method is not allowed. By default, a simple 405 response will be used.
42
+ Router ({
43
+ Handler notFoundHandler = _defaultNotFound,
44
+ Handler methodNotAllowedHandler = _defaultMethodNotAllowed,
45
+ }) : _notFoundHandler = notFoundHandler,
46
+ _methodNotAllowedHandler = methodNotAllowedHandler;
41
47
42
48
/// Name of the parameter used for matching
43
49
/// the rest of the path in a mounted route.
@@ -48,6 +54,7 @@ class Router {
48
54
49
55
final List <RouterEntry > _routes = [];
50
56
final Handler _notFoundHandler;
57
+ final Handler _methodNotAllowedHandler;
51
58
52
59
/// Add [handler] for [verb] requests to [route] .
53
60
///
@@ -167,8 +174,14 @@ class Router {
167
174
/// This method allows a Router instance to be a [Handler] .
168
175
Future <Response > call (RequestContext context) async {
169
176
for (final route in _routes) {
170
- if (route.verb != context.request.method.value.toUpperCase () &&
171
- route.verb != 'ALL' ) {
177
+ final HttpMethod method;
178
+ try {
179
+ method = context.request.method;
180
+ } on UnsupportedHttpMethodException {
181
+ return _methodNotAllowedHandler (context);
182
+ }
183
+
184
+ if (route.verb != method.value.toUpperCase () && route.verb != 'ALL' ) {
172
185
continue ;
173
186
}
174
187
final params = route.match ('/${context .request ._request .url .path }' );
@@ -211,11 +224,22 @@ class Router {
211
224
212
225
static Response _defaultNotFound (RequestContext context) => routeNotFound;
213
226
227
+ static Response _defaultMethodNotAllowed (RequestContext context) {
228
+ return methodNotAllowed;
229
+ }
230
+
214
231
/// Sentinel [Response] object indicating that no matching route was found.
215
232
///
216
233
/// This is the default response value from a [Router] created without a
217
234
/// `notFoundHandler` , when no routes matches the incoming request.
218
235
static final Response routeNotFound = _RouteNotFoundResponse ();
236
+
237
+ /// Sentinel [Response] object indicating that the http method
238
+ /// was not allowed for the requested route.
239
+ ///
240
+ /// This is the default response value from a [Router] created without a
241
+ /// `methodNotAllowedHandler` , when an unsupported http method is requested.
242
+ static final Response methodNotAllowed = _MethodNotAllowedResponse ();
219
243
}
220
244
221
245
/// Extends [Response] to allow it to be used multiple times in the
@@ -241,6 +265,29 @@ class _RouteNotFoundResponse extends Response {
241
265
}
242
266
}
243
267
268
+ /// Extends [Response] to allow it to be used multiple times in the
269
+ /// actual content being served.
270
+ class _MethodNotAllowedResponse extends Response {
271
+ _MethodNotAllowedResponse ()
272
+ : super (statusCode: HttpStatus .methodNotAllowed, body: _message);
273
+ static const _message = 'Method not allowed' ;
274
+ static final _messageBytes = utf8.encode (_message);
275
+
276
+ @override
277
+ shelf.Response get _response => super ._response.change (body: _messageBytes);
278
+
279
+ @override
280
+ Stream <List <int >> bytes () => Stream <List <int >>.value (_messageBytes);
281
+
282
+ @override
283
+ Future <String > body () async => _message;
284
+
285
+ @override
286
+ Response copyWith ({Map <String , Object ?>? headers, dynamic body}) {
287
+ return super .copyWith (headers: headers, body: body ?? _message);
288
+ }
289
+ }
290
+
244
291
/// Check if the [regexp] is non-capturing.
245
292
bool _isNoCapture (String regexp) {
246
293
// Construct a new regular expression matching anything containing regexp,
0 commit comments