@@ -36,8 +36,14 @@ class Router {
3636 ///
3737 /// The [notFoundHandler] will be invoked for requests where no matching route
3838 /// 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;
4147
4248 /// Name of the parameter used for matching
4349 /// the rest of the path in a mounted route.
@@ -48,6 +54,7 @@ class Router {
4854
4955 final List <RouterEntry > _routes = [];
5056 final Handler _notFoundHandler;
57+ final Handler _methodNotAllowedHandler;
5158
5259 /// Add [handler] for [verb] requests to [route] .
5360 ///
@@ -167,8 +174,14 @@ class Router {
167174 /// This method allows a Router instance to be a [Handler] .
168175 Future <Response > call (RequestContext context) async {
169176 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' ) {
172185 continue ;
173186 }
174187 final params = route.match ('/${context .request ._request .url .path }' );
@@ -211,11 +224,22 @@ class Router {
211224
212225 static Response _defaultNotFound (RequestContext context) => routeNotFound;
213226
227+ static Response _defaultMethodNotAllowed (RequestContext context) {
228+ return methodNotAllowed;
229+ }
230+
214231 /// Sentinel [Response] object indicating that no matching route was found.
215232 ///
216233 /// This is the default response value from a [Router] created without a
217234 /// `notFoundHandler` , when no routes matches the incoming request.
218235 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 ();
219243}
220244
221245/// Extends [Response] to allow it to be used multiple times in the
@@ -241,6 +265,29 @@ class _RouteNotFoundResponse extends Response {
241265 }
242266}
243267
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+
244291/// Check if the [regexp] is non-capturing.
245292bool _isNoCapture (String regexp) {
246293 // Construct a new regular expression matching anything containing regexp,
0 commit comments