Repository: tornadoweb/tornado
Pull request #1806 (2016-08-22 10:41:17)

implement routing mechanism
In Tornado 3, the handler specified in URLSpec could be of any callable type, which allowed to implement custom routing just like that:

``` python
self.add_handlers('.*$', [
    (r'.*', app_router)
])
```

where `app_router` contained some logic for providing an appropriate handler instance.

With Tornado 4 this is unfortunately impossible (all handlers must subclass `RequestHandler`) and the simplest way to achieve relatively the same is to subclass `_RequestDispatcher` to extend `_find_handler` method and use this custom dispatcher in `Application.start_request`.

This seems hacky enough for me, so I suggest adding the legal way to extend routing mechanism in Tornado.

The main ideas:
- move all routing logic (`Application.[_get_host_handlers, add_handlers, reverse_url]`, `_RequestDispatcher._find_handler`) into separate class — the default router
- `Application.add_handlers` becomes a proxy method for adding a handler to the default router
- `Application.add_router` allows to add additional routers to the aplication
- each router implements `find_handler` and `reverse_url` methods — application calls these methods for available routers to find the first match

I'm creating this pull request to review the idea of separating routing logic — the code itself is a working proof of concept. If the idea is OK, I'll refactor the code and add more tests, examples and documentation.


bdarnell: The idea of separating out routing logic is a good one. I've thought about doing something like this myself, although I've thought of it in the context of a larger refactor of the Application/RequestHandler interfaces. Specifically, I'm unhappy with FallbackHandler and the way you need it if you use WSGIContainer in a larger app. 

Instead of allowing the routing portion of Application to be replaced, but still have routing between the Application and RequestHandler, I've thought about introducing routing as a layer *above* the application. So instead of returning RequestHandlers, `Router.find_handler()` would return HTTPMessageDelegates. Routes would point to something like an (Application, RequestHandler) pair instead of just a RequestHandler (with the Application being implied). We would of course want some convenience methods for the current use case, and I haven't sorted out what that interface would look like, but layering things like this seems better than what we have now.

What do you think?
SuminAndrew: Well, actually there may be a problem with routing-above-application scheme that you describe:

* `Application` implements `HTTPServerConnectionDelegate`, so `Application.start_request` method is called when connection is first available (if I understand correctly, any implementation of `HTTPServerConnectionDelegate` can expect to use this method as an entry point for handling client connections).
* But to choose the actual application to which we should route the request, we must first read the headers (at least to get the uri), and this happens only after `start_request` is called and returns `HTTPMessageDelegate`, then `HTTPMessageDelegate.headers_received` method is called asynchronously by the connection handling implementation.

I may be mistaken, but it seems that routing-above-application will have to break this contract between `HTTPServerConnectionDelegate` and `HTTPMessageDelegate`. Or am I missing something?
bdarnell: Right. Some responsibilities would be shifted around: the router would now implement `HTTPServerConnectionDelegate` and `start_request`; `Application.start_request` would only remain for backwards-compatibility. The router would create an object that implements `HTTPMessageDelegate` in the same way that the Application currently creates a `tornado.web._RequestDispatcher`. I think all the pieces will still fit together when rearranged this way, but it might take some experimentation. 
SuminAndrew: OK, this seems fine. I think I'll come up with a draft by the end of the week.
SuminAndrew: I've updated the code focusing on backwards compatibility and simplicity.
Now you can create an `HTTPServer` instance with several applications (goodbye FallbackHandler):
```python
server = HTTPServer([app1, app2, appN])
server.listen(8080)
```

You can call `Application.add_router` to add additional routers for application:
```python
app = Application([
    ('default_route', DefaultRouteHandler)
])
app.add_router(ImportModuleRouter())
app.listen(8080)
```

When request is received it is passed to each application and to application routers until the first match is found.

There are still some missing pieces (for example, `HTTPServer.reverse_url` is obviously needed and it seems that `reverse_url` should be included in the Router interface) and some annoying cases (like when there's an application with `default_handler_class` defined in the middle of the list), but the main idea should be clear from this draft.
SuminAndrew: So, @bdarnell, what do you say about this concept?
SuminAndrew: Ben, thank you for your comments. That seems possible (at least from the first glance :smiley:). I think I'll have a chance to revisit the implementation on the weekend.
SuminAndrew: I've made a little bit more research.

#### 1. Rule-based routing
I made a take on the routing scheme based on rules (predicates). For example, let's say that we have a routing configuration (just as a concept):
```python
routes = [
    (host_matches(".*"), [
        (path_matches("/some/path"), SomeHandler, {}, "name"),
        # ...
    ]),
    (host_matches("test.com"), [some_other_set_of_routes])
]
```

This looks nice, but the actual implementation would be rather complex even in a simple rule:

```python
class host_matches(object):
    def __init__(self, pattern):
        # normalize the pattern
        # compile the pattern
        self.pattern = pattern

    def process_request(self, request):
        # parse request to get the host
        # this should be called only once per request, not for every rule
        return split_host_and_port(request.host)[0]

    def __call__(self, host):  # called with the result of process_request
        return self.pattern.match(host) is not None
```

Moreover, there's a special case for `Application.default_host` ([web.py#L1894](https://github.com/tornadoweb/tornado/blob/master/tornado/web.py#L1894)), which doesn't fit nicely into this scheme and for `path_matches` rule to work properly it must return a match, not a boolean ([web.py#L2022](https://github.com/tornadoweb/tornado/blob/master/tornado/web.py#L2022)) and there still would be some router-level logic to handle the match.

Moreover, one of the useful routing cases is automatic routing to the appropriate module, based on the request path (when /some/request is routed to some.request.Handler). Or even building a routing configuration based on decorators (flask-like). This unfortunately can't be done with the rule-based routing alone.

All this can be solved with a general `Router.find_handler` interface in much more efficient (and Ithink that routing footprint must be as tiny as possible) and concise way, so it seems that it is not a very perspective direction.

#### 2. Nested routers
Then I had some fun with nested routers: [routing_test.py#L92](https://github.com/tornadoweb/tornado/blob/ecedc3e25cf49ca8b406bb57f9adfc4784876e3c/tornado/test/routing_test.py#L92)

This adds complexity, but after all it is possible to separate HostRouter from PathRouter. I'm not completely sure whether it's worth it or not, but I'll definitely dig into it further.

It seems wrong to pass control from one router to another through `start_request`, because it makes sense only for the top-level router. 

`Router.start_request` creates an `HTTPMessageDelegate` object, which waits for the request headers and executes the `Router` routing code. Then when we need to transfer control to a nested router, it is more logical to call `nested.find_handler(request)` rather than `nested.start_request` (the latter would produce another `HTTPMessageDelegate` and then immediately call `HTTPMD.headers_received` — 'cause they are indeed already received).

#### 3. One router per application
Unfortunately there's this: [web.py#L1811](https://github.com/tornadoweb/tornado/blob/master/tornado/web.py#L1811) which basically forces you to have at least a default path-router for an application (and then your custom router), otherwise any custom router must implement this static files routing logic.

It's much simpler (and safer from a backwards compatibility perspective) to have a default router and an ability to specify an additional custom router.

Actually, there's a use-case for a several-router configuration: we have an app that first tries to route the request based on URLSpec-like patterns (but considering request.uri instead of request.path) and then falls back to module-import router that I described earlier. Basically the latter is the main routing system and the former serves as some kind of overrides, but conceptually these are two different routers.

#### 4. So, what's in the next iteration?
First of all, now `HTTPServer` is completely independent of any knowledge about the applications or routers, it only works with the `HTTPServerConnectionDelegate` interface. `_CallableAdapter` and `_ProxyAdapter` are useful on their own — they make the code more readable and can speed it up a little (actually this could be moved to a separate PR, what do you think of that?).

`Application` now implements `Router` interface (which is in its turn a `HTTPServerConnectionDelegate`), this seems logical because if an application can contain a router (or a set of routers), to which it delegates routing logic, then why can't it _be_ the router?

Routers return `HTTPMessageDelegate` implementation — `_HandlerDelegate` (essentially a `_RequestDispatcher` stripped of any routing logic) with the help of `Application.get_handler_delegate` method.

There's some awkwardness in implementation of the router which combines several applications — you  mustn't return 404 when there's still other routers available. Off the top of my head I worked around it with a `has_next_router` flag, but I'm sure there's a more elegant solution out there.

I'm looking forward to your comments — I'll have a chance to continue this experiment after Tuesday.
SuminAndrew: So, @bdarnell, what do you think of all that?
bdarnell: 1. Rule-based routing

For common uses of `process_request()` (e.g. `split_host_and_port()`), we could just add properties to the request object itself (perhaps lazy and cached). But maybe we need a way to have the router augment the request (this would give `path_matches()` a place to put its regex information). There are a few different ways we could go with this (just set attributes on the request directly, or give them a separate "properties" dict to avoid collisions? Mutate the incoming request or treat it as immutable and return a new modified value?), but let's think about it and see if it makes sense.

The `default_host` feature is honestly a weird hack (it's kind of bit-rotted and diverged from other related features - I'm not sure if anyone but Friendfeed ever used it) and I'm OK with it being a second-class citizen in the new world. One way to implement it would be for one of the rules in the host matcher to have a `default` attribute to be used when we think we're in a development environment. It's fine with me if the `default` attribute is awkward to set and is only used when `Application` constructs a router tree for you when `default_host` is in use. I'd prefer to steer towards providing this feature by explicitly adding wildcard matchers into the application's routing table. 

> Moreover, one of the useful routing cases is automatic routing to the appropriate module, based on the request path (when /some/request is routed to some.request.Handler). 

Right, this would be implemented with a specialized Router subclass instead of using the rule-based system in the default Router. (FWIW, I dislike both this style of routing and the flask decorator style, but I would like the routing system to be flexible enough that third parties could provide/use them)

> Or even building a routing configuration based on decorators (flask-like). This unfortunately can't be done with the rule-based routing alone.

Why not? Your decorators could accumulate a list of rules into a global variable. At least a few people are doing that with tornado today.

2. Nested routers

I agree that this is probably a better way to go. The HostRouter/PathRouter separation also gives you a place to plug in other things like an AutoPathRouter that maps /some/path to `some.path.Handler`

I like connecting the routers through the `start_request`/`headers_received` interface because that means that the top-level router is not special and it makes it easier to plug in things like `WSGIContainer` (compare to the `FallbackHandler` hack we have today, to translate the `RequestHandler` interface back into the top-level `HTTPMessageDelegate` interface). 

3. One router per application

When `Application` constructs a router tree for you, it should insert the static rules like it does now. If you're constructing the router tree outside the application, it can be done differently. I think I'd like for static files to be represented explicitly in the route list, or maybe it's a feature of PathRouter. 


SuminAndrew: @bdarnell, thank you for the response!

_Rule-based routing_ — ok, let's see where this takes us. In a draft I had a PredicateRouter and HostRouter/PathRouter extending it, that looked rather neat. Except for the default_host, but I'll try to make a not so ugly workaround for this.

_One router per application_ — I guess we could even have a special StaticRouter. Application could then first consider the static routes and then proceed to the main router. Will look into that.

Also, what do you think about the changes in http_server? Are they worth a separate pull request? I guess they make the code easier to understand.
SuminAndrew: I just pushed a commit with the next iteration of the code.
Now there's a rule-based routing, a tree of routers inside an application and an ability to implement custom routers.

I had to get rid of one (rather weird) default_host behavior: a redirect to default_host if no host match is found. It was completely broken when default_host was not set, redirecting to http:/// and I can't imagine a useful case for which this redirect would be more suitable than a normal 404.

I added dome unittests for default_host's current behavior in https://github.com/tornadoweb/tornado/pull/1857 and modified them in this PR.
SuminAndrew: @bdarnell, what do you think of this?
SuminAndrew: Thank you for the comments @bdarnell , I'll fix them as soon as possible with some more tests and docs.
SuminAndrew: That's good news! I'll be adding docs and comments then and addressing the remaining issues.

Patch:
diff --git a/demos/file_upload/file_receiver.py b/demos/file_upload/file_receiver.py
deleted file mode 100644
index 3b3e9867..00000000
--- a/demos/file_upload/file_receiver.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env python
-
-"""Usage: python file_receiver.py
-
-Demonstrates a server that receives a multipart-form-encoded set of files in an
-HTTP POST, or streams in the raw data of a single file in an HTTP PUT.
-
-See file_uploader.py in this directory for code that uploads files in this format.
-"""
-
-import logging
-
-try:
-    from urllib.parse import unquote
-except ImportError:
-    # Python 2.
-    from urllib import unquote
-
-import tornado.ioloop
-import tornado.web
-from tornado import options
-
-
-class POSTHandler(tornado.web.RequestHandler):
-    def post(self):
-        for field_name, files in self.request.files.items():
-            for info in files:
-                filename, content_type = info['filename'], info['content_type']
-                body = info['body']
-                logging.info('POST "%s" "%s" %d bytes',
-                             filename, content_type, len(body))
-
-        self.write('OK')
-
-
-@tornado.web.stream_request_body
-class PUTHandler(tornado.web.RequestHandler):
-    def initialize(self):
-        self.bytes_read = 0
-
-    def data_received(self, chunk):
-        self.bytes_read += len(chunk)
-
-    def put(self, filename):
-        filename = unquote(filename)
-        mtype = self.request.headers.get('Content-Type')
-        logging.info('PUT "%s" "%s" %d bytes', filename, mtype, self.bytes_read)
-        self.write('OK')
-
-
-def make_app():
-    return tornado.web.Application([
-        (r"/post", POSTHandler),
-        (r"/(.*)", PUTHandler),
-    ])
-
-
-if __name__ == "__main__":
-    # Tornado configures logging.
-    options.parse_command_line()
-    app = make_app()
-    app.listen(8888)
-    tornado.ioloop.IOLoop.current().start()
diff --git a/demos/file_upload/file_uploader.py b/demos/file_upload/file_uploader.py
deleted file mode 100644
index 025c2159..00000000
--- a/demos/file_upload/file_uploader.py
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/env python
-
-"""Usage: python file_uploader.py [--put] file1.txt file2.png ...
-
-Demonstrates uploading files to a server, without concurrency. It can either
-POST a multipart-form-encoded request containing one or more files, or PUT a
-single file without encoding.
-
-See also file_receiver.py in this directory, a server that receives uploads.
-"""
-
-import mimetypes
-import os
-import sys
-from functools import partial
-from uuid import uuid4
-
-try:
-    from urllib.parse import quote
-except ImportError:
-    # Python 2.
-    from urllib import quote
-
-from tornado import gen, httpclient, ioloop
-from tornado.options import define, options
-
-
-# Using HTTP POST, upload one or more files in a single multipart-form-encoded
-# request.
-@gen.coroutine
-def multipart_producer(boundary, filenames, write):
-    boundary_bytes = boundary.encode()
-
-    for filename in filenames:
-        filename_bytes = filename.encode()
-        write(b'--%s\r\n' % (boundary_bytes,))
-        write(b'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' %
-              (filename_bytes, filename_bytes))
-
-        mtype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
-        write(b'Content-Type: %s\r\n' % (mtype.encode(),))
-        write(b'\r\n')
-        with open(filename, 'rb') as f:
-            while True:
-                # 16k at a time.
-                chunk = f.read(16 * 1024)
-                if not chunk:
-                    break
-                write(chunk)
-
-                # Let the IOLoop process its event queue.
-                yield gen.moment
-
-        write(b'\r\n')
-        yield gen.moment
-
-    write(b'--%s--\r\n' % (boundary_bytes,))
-
-
-# Using HTTP PUT, upload one raw file. This is preferred for large files since
-# the server can stream the data instead of buffering it entirely in memory.
-@gen.coroutine
-def post(filenames):
-    client = httpclient.AsyncHTTPClient()
-    boundary = uuid4().hex
-    headers = {'Content-Type': 'multipart/form-data; boundary=%s' % boundary}
-    producer = partial(multipart_producer, boundary, filenames)
-    response = yield client.fetch('http://localhost:8888/post',
-                                  method='POST',
-                                  headers=headers,
-                                  body_producer=producer)
-
-    print(response)
-
-
-@gen.coroutine
-def raw_producer(filename, write):
-    with open(filename, 'rb') as f:
-        while True:
-            # 16K at a time.
-            chunk = f.read(16 * 1024)
-            if not chunk:
-                # Complete.
-                break
-
-            write(chunk)
-
-
-@gen.coroutine
-def put(filenames):
-    client = httpclient.AsyncHTTPClient()
-    for filename in filenames:
-        mtype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
-        headers = {'Content-Type': mtype}
-        producer = partial(raw_producer, filename)
-        url_path = quote(os.path.basename(filename))
-        response = yield client.fetch('http://localhost:8888/%s' % url_path,
-                                      method='PUT',
-                                      headers=headers,
-                                      body_producer=producer)
-    
-        print(response)
-
-
-define("put", type=bool, help="Use PUT instead of POST", group="file uploader")
-
-# Tornado configures logging from command line opts and returns remaining args.
-filenames = options.parse_command_line()
-if not filenames:
-    print("Provide a list of filenames to upload.", file=sys.stderr)
-    sys.exit(1)
-
-method = put if options.put else post
-ioloop.IOLoop.current().run_sync(lambda: method(filenames))
diff --git a/docs/gen.rst b/docs/gen.rst
index 52c7f55d..8e867ed0 100644
--- a/docs/gen.rst
+++ b/docs/gen.rst
@@ -50,8 +50,6 @@
 
    .. autofunction:: maybe_future
 
-   .. autofunction:: is_coroutine_function
-
    Legacy interface
    ----------------
 
diff --git a/docs/guide/structure.rst b/docs/guide/structure.rst
index b0735a17..715e080f 100644
--- a/docs/guide/structure.rst
+++ b/docs/guide/structure.rst
@@ -153,10 +153,6 @@ By default uploaded files are fully buffered in memory; if you need to
 handle files that are too large to comfortably keep in memory see the
 `.stream_request_body` class decorator.
 
-In the demos directory,
-`file_receiver.py <https://github.com/tornadoweb/tornado/tree/master/demos/file_upload/>`_
-shows both methods of receiving file uploads.
-
 Due to the quirks of the HTML form encoding (e.g. the ambiguity around
 singular versus plural arguments), Tornado does not attempt to unify
 form arguments with other types of input.  In particular, we do not
diff --git a/docs/httpclient.rst b/docs/httpclient.rst
index 53a0a881..a641fa29 100644
--- a/docs/httpclient.rst
+++ b/docs/httpclient.rst
@@ -50,11 +50,3 @@ Implementations
 .. class:: CurlAsyncHTTPClient(io_loop, max_clients=10, defaults=None)
 
    ``libcurl``-based HTTP client.
-
-Example Code
-~~~~~~~~~~~~
-
-* `A simple webspider <https://github.com/tornadoweb/tornado/blob/master/demos/webspider/webspider.py>`_
-  shows how to fetch URLs concurrently.
-* `The file uploader demo <https://github.com/tornadoweb/tornado/tree/master/demos/file_upload/>`_
-  uses either HTTP POST or HTTP PUT to upload files to a server.
diff --git a/docs/releases/v2.3.0.rst b/docs/releases/v2.3.0.rst
index 368ceec9..d24f46c5 100644
--- a/docs/releases/v2.3.0.rst
+++ b/docs/releases/v2.3.0.rst
@@ -80,7 +80,7 @@ HTTP Server
   backwards-incompatible change to an interface that was never technically
   private, but was not included in the documentation and does not appear
   to have been used outside Tornado itself.
-* Fixed a bug on python versions before 2.6.5 when `.URLSpec` regexes
+* Fixed a bug on python versions before 2.6.5 when `tornado.web.URLSpec` regexes
   are constructed from unicode strings and keyword arguments are extracted.
 * The ``reverse_url`` function in the template namespace now comes from
   the `.RequestHandler` rather than the `.Application`.  (Unless overridden,
diff --git a/docs/releases/v3.2.0.rst b/docs/releases/v3.2.0.rst
index 95db3e98..09057030 100644
--- a/docs/releases/v3.2.0.rst
+++ b/docs/releases/v3.2.0.rst
@@ -164,11 +164,11 @@ New modules
   argument could not be decoded.
 * `.RequestHandler.clear_all_cookies` now accepts ``domain`` and ``path``
   arguments, just like `~.RequestHandler.clear_cookie`.
-* It is now possible to specify handlers by name when using the `.URLSpec`
-  class.
+* It is now possible to specify handlers by name when using the
+  `tornado.web.URLSpec` class.
 * `.Application` now accepts 4-tuples to specify the ``name`` parameter
-  (which previously required constructing a `.URLSpec` object instead of
-  a tuple).
+  (which previously required constructing a `tornado.web.URLSpec` object
+  instead of a tuple).
 * Fixed an incorrect error message when handler methods return a value
   other than None or a Future.
 * Exceptions will no longer be logged twice when using both ``@asynchronous``
diff --git a/docs/routing.rst b/docs/routing.rst
new file mode 100644
index 00000000..ec6b0ca3
--- /dev/null
+++ b/docs/routing.rst
@@ -0,0 +1,5 @@
+``tornado.routing`` --- Basic routing implementation
+====================================================
+
+.. automodule:: tornado.routing
+   :members:
diff --git a/docs/webframework.rst b/docs/webframework.rst
index ab3ff1cf..ab93ccb2 100644
--- a/docs/webframework.rst
+++ b/docs/webframework.rst
@@ -5,6 +5,7 @@ Web framework
 
    web
    template
+   routing
    escape
    locale
    websocket
diff --git a/tornado/concurrent.py b/tornado/concurrent.py
index ec68dc4f..05205f73 100644
--- a/tornado/concurrent.py
+++ b/tornado/concurrent.py
@@ -31,7 +31,7 @@ import sys
 
 from tornado.log import app_log
 from tornado.stack_context import ExceptionStackContext, wrap
-from tornado.util import raise_exc_info, ArgReplacer, is_finalizing
+from tornado.util import raise_exc_info, ArgReplacer
 
 try:
     from concurrent import futures
@@ -123,8 +123,8 @@ class _TracebackLogger(object):
         self.exc_info = None
         self.formatted_tb = None
 
-    def __del__(self, is_finalizing=is_finalizing):
-        if not is_finalizing() and self.formatted_tb:
+    def __del__(self):
+        if self.formatted_tb:
             app_log.error('Future exception was never retrieved: %s',
                           ''.join(self.formatted_tb).rstrip())
 
@@ -329,8 +329,8 @@ class Future(object):
     # cycle are never destroyed. It's no longer the case on Python 3.4 thanks to
     # the PEP 442.
     if _GC_CYCLE_FINALIZERS:
-        def __del__(self, is_finalizing=is_finalizing):
-            if is_finalizing() or not self._log_traceback:
+        def __del__(self):
+            if not self._log_traceback:
                 # set_exception() was not called, or result() or exception()
                 # has consumed the exception
                 return
diff --git a/tornado/gen.py b/tornado/gen.py
index d7df3b52..73f9ba10 100644
--- a/tornado/gen.py
+++ b/tornado/gen.py
@@ -273,11 +273,10 @@ def _make_coroutine_wrapper(func, replace_callback):
     """
     # On Python 3.5, set the coroutine flag on our generator, to allow it
     # to be used with 'await'.
-    wrapped = func
     if hasattr(types, 'coroutine'):
         func = types.coroutine(func)
 
-    @functools.wraps(wrapped)
+    @functools.wraps(func)
     def wrapper(*args, **kwargs):
         future = TracebackFuture()
 
@@ -329,19 +328,9 @@ def _make_coroutine_wrapper(func, replace_callback):
                     future = None
         future.set_result(result)
         return future
-
-    wrapper.__wrapped__ = wrapped
-    wrapper.__tornado_coroutine__ = True
     return wrapper
 
 
-def is_coroutine_function(func):
-    """Return whether *func* is a coroutine function, i.e. a function
-    wrapped with `~.gen.coroutine`.
-    """
-    return getattr(func, '__tornado_coroutine__', False)
-
-
 class Return(Exception):
     """Special exception to return a value from a `coroutine`.
 
diff --git a/tornado/httputil.py b/tornado/httputil.py
index 21842caa..8ea8a01e 100644
--- a/tornado/httputil.py
+++ b/tornado/httputil.py
@@ -337,7 +337,7 @@ class HTTPServerRequest(object):
     """
     def __init__(self, method=None, uri=None, version="HTTP/1.0", headers=None,
                  body=None, host=None, files=None, connection=None,
-                 start_line=None):
+                 start_line=None, server_connection=None):
         if start_line is not None:
             method, uri, version = start_line
         self.method = method
@@ -352,8 +352,10 @@ class HTTPServerRequest(object):
         self.protocol = getattr(context, 'protocol', "http")
 
         self.host = host or self.headers.get("Host") or "127.0.0.1"
+        self.host_name = split_host_and_port(self.host.lower())[0]
         self.files = files or {}
         self.connection = connection
+        self.server_connection = server_connection
         self._start_time = time.time()
         self._finish_time = None
 
diff --git a/tornado/routing.py b/tornado/routing.py
new file mode 100644
index 00000000..15baeb48
--- /dev/null
+++ b/tornado/routing.py
@@ -0,0 +1,545 @@
+# Copyright 2015 The Tornado Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Basic routing implementation.
+
+Tornado routes HTTP requests to appropriate handlers using `Router` class implementations.
+
+Any `Router` implementation can be used directly as a ``request_callback`` for
+`~.httpserver.HTTPServer` (this is possible because `Router` implements
+`~.httputil.HTTPServerConnectionDelegate`):
+
+.. code-block:: python
+
+    class CustomRouter(Router):
+        def find_handler(self, request, **kwargs):
+            # some routing logic providing a suitable HTTPMessageDelegate implementation
+            return HTTPMessageDelegateImplementation()
+
+    router = CustomRouter()
+    server = HTTPServer(router)
+
+`Router.find_handler` is the only method that you must implement to provide routing
+logic in its simplest case. This method must return an instance of
+`~.httputil.HTTPMessageDelegate` that will be used to process the request.
+
+`ReversibleRouter` interface adds the ability to distinguish between the routes and
+reverse them to the original urls using route's name and additional arguments.
+`~.web.Application` is itself an implementation of `ReversibleRouter` class.
+
+`RuleRouter` and `ReversibleRuleRouter` provide an interface for creating rule-based
+routing configurations. For example, `RuleRouter` can be used to route between applications:
+
+.. code-block:: python
+
+    app1 = Application([
+        (r"/app1/handler1", Handler1),
+        # other handlers ...
+    ])
+
+    app2 = Application([
+        (r"/app2/handler1", Handler1),
+        # other handlers ...
+    ])
+
+    router = RuleRouter([
+        Rule(PathMatches("/app1.*"), app1),
+        Rule(PathMatches("/app2.*"), app2)
+    ])
+
+    server = HTTPServer(router)
+
+Implementations of `~.httputil.HTTPServerConnectionDelegate` and old-style callables can also be used as
+rule targets:
+
+.. code-block:: python
+
+    router = RuleRouter([
+        Rule(PathMatches("/callable"), request_callable),  # def request_callable(request): ...
+        Rule(PathMatches("/delegate"), HTTPServerConnectionDelegateImpl())
+    ])
+
+    server = HTTPServer(router)
+
+You can use nested routers as targets as well:
+
+.. code-block:: python
+
+    router = RuleRouter([
+        Rule(PathMatches("/router.*"), CustomRouter())
+    ])
+
+    server = HTTPServer(router)
+
+And of course a nested `RuleRouter` would be a valid thing:
+
+.. code-block:: python
+
+    router = RuleRouter([
+        Rule(HostMatches("example.com"), RuleRouter([
+            Rule(PathMatches("/app1/.*"), Application([(r"/app1/handler", Handler)]))),
+        ]))
+    ])
+
+    server = HTTPServer(router)
+
+Rules are instances of `Rule` class. They contain some target (`~.web.Application` instance,
+`~.httputil.HTTPServerConnectionDelegate` implementation, a callable or a nested `Router`) and
+provide the basic routing logic defining whether this rule is a match for a particular request.
+This routing logic is implemented in `Matcher` subclasses (see `HostMatches`, `PathMatches`
+and others).
+
+`~URLSpec` is simply a subclass of a `Rule` with `PathMatches` matcher and is preserved for
+backwards compatibility.
+"""
+
+from __future__ import absolute_import, division, print_function, with_statement
+
+import re
+from functools import partial
+
+from tornado import httputil
+from tornado.httpserver import _CallableAdapter
+from tornado.escape import url_escape, url_unescape, utf8
+from tornado.log import app_log
+from tornado.util import basestring_type, import_object, re_unescape, unicode_type
+
+try:
+    import typing  # noqa
+except ImportError:
+    pass
+
+
+class Router(httputil.HTTPServerConnectionDelegate):
+    """Abstract router interface."""
+
+    def find_handler(self, request, **kwargs):
+        # type: (httputil.HTTPServerRequest, typing.Any)->httputil.HTTPMessageDelegate
+        """Must be implemented to return an appropriate instance of `~.httputil.HTTPMessageDelegate`
+        that can serve the request.
+        Routing implementations may pass additional kwargs to extend the routing logic.
+
+        :arg httputil.HTTPServerRequest request: current HTTP request.
+        :arg kwargs: additional keyword arguments passed by routing implementation.
+        :returns: an instance of `~.httputil.HTTPMessageDelegate` that will be used to
+            process the request.
+        """
+        raise NotImplementedError()
+
+    def start_request(self, server_conn, request_conn):
+        return _RoutingDelegate(self, server_conn, request_conn)
+
+
+class ReversibleRouter(Router):
+    """Abstract router interface for routers that can handle named routes
+    and support reversing them to original urls.
+    """
+
+    def reverse_url(self, name, *args):
+        """Returns url string for a given route name and arguments
+        or ``None`` if no match is found.
+
+        :arg str name: route name.
+        :arg args: url parameters.
+        :returns: parametrized url string for a given route name (or ``None``).
+        """
+        raise NotImplementedError()
+
+
+class _RoutingDelegate(httputil.HTTPMessageDelegate):
+    def __init__(self, router, server_conn, request_conn):
+        self.server_conn = server_conn
+        self.request_conn = request_conn
+        self.delegate = None
+        self.router = router  # type: Router
+
+    def headers_received(self, start_line, headers):
+        request = httputil.HTTPServerRequest(
+            connection=self.request_conn,
+            server_connection=self.server_conn,
+            start_line=start_line, headers=headers)
+
+        self.delegate = self.router.find_handler(request)
+        return self.delegate.headers_received(start_line, headers)
+
+    def data_received(self, chunk):
+        return self.delegate.data_received(chunk)
+
+    def finish(self):
+        self.delegate.finish()
+
+    def on_connection_close(self):
+        self.delegate.on_connection_close()
+
+
+class RuleRouter(Router):
+    """Rule-based router implementation."""
+
+    def __init__(self, rules=None):
+        """Constructs a router with an ordered list of rules::
+
+            RuleRouter([
+                Rule(PathMatches("/handler"), SomeHandler),
+                # ... more rules
+            ])
+
+        You can also omit explicit `Rule` constructor and use tuples of arguments::
+
+            RuleRouter([
+                (PathMatches("/handler"), SomeHandler),
+            ])
+
+        `PathMatches` is a default matcher, so the example above can be simplified::
+
+            RuleRouter([
+                ("/handler", SomeHandler),
+            ])
+
+        :arg rules: a list of `Rule` instances or tuples of `Rule`
+            constructor arguments.
+        """
+        self.rules = []  # type: typing.List[Rule]
+        if rules:
+            self.add_rules(rules)
+
+    def add_rules(self, rules):
+        """Appends new rules to the router.
+
+        :arg rules: a list of Rule instances (or tuples of arguments, which are
+            passed to Rule constructor).
+        """
+        for rule in rules:
+            if isinstance(rule, (tuple, list)):
+                assert len(rule) in (2, 3, 4)
+                if isinstance(rule[0], basestring_type):
+                    rule = Rule(PathMatches(rule[0]), *rule[1:])
+                else:
+                    rule = Rule(*rule)
+
+            self.rules.append(self.process_rule(rule))
+
+    def process_rule(self, rule):
+        """Override this method for additional preprocessing of each rule.
+
+        :arg Rule rule: a rule to be processed.
+        :returns: the same or modified Rule instance.
+        """
+        return rule
+
+    def find_handler(self, request, **kwargs):
+        for rule in self.rules:
+            target_params = rule.matcher.match(request)
+            if target_params is not None:
+                if rule.target_kwargs:
+                    target_params['target_kwargs'] = rule.target_kwargs
+
+                delegate = self.get_target_delegate(
+                    rule.target, request, **target_params)
+
+                if delegate is not None:
+                    return delegate
+
+        return None
+
+    def get_target_delegate(self, target, request, **target_params):
+        """Returns an instance of `~.httputil.HTTPMessageDelegate` for a
+        Rule's target. This method is called by `~.find_handler` and can be
+        extended to provide additional target types.
+
+        :arg target: a Rule's target.
+        :arg httputil.HTTPServerRequest request: current request.
+        :arg target_params: additional parameters that can be useful
+            for `~.httputil.HTTPMessageDelegate` creation.
+        """
+        if isinstance(target, Router):
+            return target.find_handler(request, **target_params)
+
+        elif isinstance(target, httputil.HTTPServerConnectionDelegate):
+            return target.start_request(request.server_connection, request.connection)
+
+        elif callable(target):
+            return _CallableAdapter(
+                partial(target, **target_params), request.connection
+            )
+
+        return None
+
+
+class ReversibleRuleRouter(ReversibleRouter, RuleRouter):
+    """A rule-based router that implements ``reverse_url`` method.
+
+    Each rule added to this router may have a ``name`` attribute that can be
+    used to reconstruct an original uri. The actual reconstruction takes place
+    in a rule's matcher (see `Matcher.reverse`).
+    """
+
+    def __init__(self, rules=None):
+        self.named_rules = {}  # type: typing.Dict[str]
+        super(ReversibleRuleRouter, self).__init__(rules)
+
+    def process_rule(self, rule):
+        rule = super(ReversibleRuleRouter, self).process_rule(rule)
+
+        if rule.name:
+            if rule.name in self.named_rules:
+                app_log.warning(
+                    "Multiple handlers named %s; replacing previous value",
+                    rule.name)
+            self.named_rules[rule.name] = rule
+
+        return rule
+
+    def reverse_url(self, name, *args):
+        if name in self.named_rules:
+            return self.named_rules[name].matcher.reverse(*args)
+
+        for rule in self.rules:
+            if isinstance(rule.target, ReversibleRouter):
+                reversed_url = rule.target.reverse_url(name, *args)
+                if reversed_url is not None:
+                    return reversed_url
+
+        return None
+
+
+class Rule(object):
+    """A routing rule."""
+
+    def __init__(self, matcher, target, target_kwargs=None, name=None):
+        """Constructs a Rule instance.
+
+        :arg Matcher matcher: a `Matcher` instance used for determining
+            whether the rule should be considered a match for a specific
+            request.
+        :arg target: a Rule's target (typically a ``RequestHandler`` or
+            `~.httputil.HTTPServerConnectionDelegate` subclass or even a nested `Router`).
+        :arg dict target_kwargs: a dict of parameters that can be useful
+            at the moment of target instantiation (for example, ``status_code``
+            for a ``RequestHandler`` subclass). They end up in
+            ``target_params['target_kwargs']`` of `RuleRouter.get_target_delegate`
+            method.
+        :arg str name: the name of the rule that can be used to find it
+            in `ReversibleRouter.reverse_url` implementation.
+        """
+        if isinstance(target, str):
+            # import the Module and instantiate the class
+            # Must be a fully qualified name (module.ClassName)
+            target = import_object(target)
+
+        self.matcher = matcher  # type: Matcher
+        self.target = target
+        self.target_kwargs = target_kwargs if target_kwargs else {}
+        self.name = name
+
+    def reverse(self, *args):
+        return self.matcher.reverse(*args)
+
+    def __repr__(self):
+        return '%s(%r, %s, kwargs=%r, name=%r)' % \
+               (self.__class__.__name__, self.matcher,
+                self.target, self.target_kwargs, self.name)
+
+
+class Matcher(object):
+    """Represents a matcher for request features."""
+
+    def match(self, request):
+        """Matches current instance against the request.
+
+        :arg httputil.HTTPServerRequest request: current HTTP request
+        :returns: a dict of parameters to be passed to the target handler
+            (for example, ``handler_kwargs``, ``path_args``, ``path_kwargs``
+            can be passed for proper `~.web.RequestHandler` instantiation).
+            An empty dict is a valid (and common) return value to indicate a match
+            when the argument-passing features are not used.
+            ``None`` must be returned to indicate that there is no match."""
+        raise NotImplementedError()
+
+    def reverse(self, *args):
+        """Reconstructs full url from matcher instance and additional arguments."""
+        return None
+
+
+class AnyMatches(Matcher):
+    """Matches any request."""
+
+    def match(self, request):
+        return {}
+
+
+class HostMatches(Matcher):
+    """Matches requests from hosts specified by ``host_pattern`` regex."""
+
+    def __init__(self, host_pattern):
+        if isinstance(host_pattern, basestring_type):
+            if not host_pattern.endswith("$"):
+                host_pattern += "$"
+            self.host_pattern = re.compile(host_pattern)
+        else:
+            self.host_pattern = host_pattern
+
+    def match(self, request):
+        if self.host_pattern.match(request.host_name):
+            return {}
+
+        return None
+
+
+class DefaultHostMatches(Matcher):
+    """Matches requests from host that is equal to application's default_host.
+    Always returns no match if ``X-Real-Ip`` header is present.
+    """
+
+    def __init__(self, application, host_pattern):
+        self.application = application
+        self.host_pattern = host_pattern
+
+    def match(self, request):
+        # Look for default host if not behind load balancer (for debugging)
+        if "X-Real-Ip" not in request.headers:
+            if self.host_pattern.match(self.application.default_host):
+                return {}
+        return None
+
+
+class PathMatches(Matcher):
+    """Matches requests with paths specified by ``path_pattern`` regex."""
+
+    def __init__(self, path_pattern):
+        if isinstance(path_pattern, basestring_type):
+            if not path_pattern.endswith('$'):
+                path_pattern += '$'
+            self.regex = re.compile(path_pattern)
+        else:
+            self.regex = path_pattern
+
+        assert len(self.regex.groupindex) in (0, self.regex.groups), \
+            ("groups in url regexes must either be all named or all "
+             "positional: %r" % self.regex.pattern)
+
+        self._path, self._group_count = self._find_groups()
+
+    def match(self, request):
+        match = self.regex.match(request.path)
+        if match is None:
+            return None
+        if not self.regex.groups:
+            return {}
+
+        path_args, path_kwargs = [], {}
+
+        # Pass matched groups to the handler.  Since
+        # match.groups() includes both named and
+        # unnamed groups, we want to use either groups
+        # or groupdict but not both.
+        if self.regex.groupindex:
+            path_kwargs = dict(
+                (str(k), _unquote_or_none(v))
+                for (k, v) in match.groupdict().items())
+        else:
+            path_args = [_unquote_or_none(s) for s in match.groups()]
+
+        return dict(path_args=path_args, path_kwargs=path_kwargs)
+
+    def reverse(self, *args):
+        if self._path is None:
+            raise ValueError("Cannot reverse url regex " + self.regex.pattern)
+        assert len(args) == self._group_count, "required number of arguments " \
+                                               "not found"
+        if not len(args):
+            return self._path
+        converted_args = []
+        for a in args:
+            if not isinstance(a, (unicode_type, bytes)):
+                a = str(a)
+            converted_args.append(url_escape(utf8(a), plus=False))
+        return self._path % tuple(converted_args)
+
+    def _find_groups(self):
+        """Returns a tuple (reverse string, group count) for a url.
+
+        For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
+        would return ('/%s/%s/', 2).
+        """
+        pattern = self.regex.pattern
+        if pattern.startswith('^'):
+            pattern = pattern[1:]
+        if pattern.endswith('$'):
+            pattern = pattern[:-1]
+
+        if self.regex.groups != pattern.count('('):
+            # The pattern is too complicated for our simplistic matching,
+            # so we can't support reversing it.
+            return None, None
+
+        pieces = []
+        for fragment in pattern.split('('):
+            if ')' in fragment:
+                paren_loc = fragment.index(')')
+                if paren_loc >= 0:
+                    pieces.append('%s' + fragment[paren_loc + 1:])
+            else:
+                try:
+                    unescaped_fragment = re_unescape(fragment)
+                except ValueError as exc:
+                    # If we can't unescape part of it, we can't
+                    # reverse this url.
+                    return (None, None)
+                pieces.append(unescaped_fragment)
+
+        return ''.join(pieces), self.regex.groups
+
+
+class URLSpec(Rule):
+    """Specifies mappings between URLs and handlers."""
+    def __init__(self, pattern, handler, kwargs=None, name=None):
+        """Parameters:
+
+        * ``pattern``: Regular expression to be matched. Any capturing
+          groups in the regex will be passed in to the handler's
+          get/post/etc methods as arguments (by keyword if named, by
+          position if unnamed. Named and unnamed capturing groups may
+          may not be mixed in the same rule).
+
+        * ``handler``: `~.web.RequestHandler` subclass to be invoked.
+
+        * ``kwargs`` (optional): A dictionary of additional arguments
+          to be passed to the handler's constructor.
+
+        * ``name`` (optional): A name for this handler.  Used by
+          `~.web.Application.reverse_url`.
+
+        """
+        super(URLSpec, self).__init__(PathMatches(pattern), handler, kwargs, name)
+
+        self.regex = self.matcher.regex
+        self.handler_class = self.target
+        self.kwargs = kwargs
+
+    def __repr__(self):
+        return '%s(%r, %s, kwargs=%r, name=%r)' % \
+               (self.__class__.__name__, self.regex.pattern,
+                self.handler_class, self.kwargs, self.name)
+
+
+def _unquote_or_none(s):
+    """None-safe wrapper around url_unescape to handle unmatched optional
+    groups correctly.
+
+    Note that args are passed as bytes so the handler can decide what
+    encoding to use.
+    """
+    if s is None:
+        return s
+    return url_unescape(s, encoding=None, plus=False)
diff --git a/tornado/util.py b/tornado/util.py
index d0f83d1f..28e74e7d 100644
--- a/tornado/util.py
+++ b/tornado/util.py
@@ -13,7 +13,6 @@ and `.Resolver`.
 from __future__ import absolute_import, division, print_function, with_statement
 
 import array
-import atexit
 import os
 import re
 import sys
@@ -67,23 +66,6 @@ else:
         _BaseString = Union[bytes, unicode_type]
 
 
-try:
-    from sys import is_finalizing
-except ImportError:
-    # Emulate it
-    def _get_emulated_is_finalizing():
-        L = []
-        atexit.register(lambda: L.append(None))
-
-        def is_finalizing():
-            # Not referencing any globals here
-            return L != []
-
-        return is_finalizing
-
-    is_finalizing = _get_emulated_is_finalizing()
-
-
 class ObjectDict(_ObjectDictBase):
     """Makes a dictionary behave like an object, with attribute-style access.
     """
diff --git a/tornado/web.py b/tornado/web.py
index 65e32016..6552c130 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -77,6 +77,7 @@ import time
 import tornado
 import traceback
 import types
+from inspect import isclass
 from io import BytesIO
 
 from tornado.concurrent import Future
@@ -89,9 +90,13 @@ from tornado.log import access_log, app_log, gen_log
 from tornado import stack_context
 from tornado import template
 from tornado.escape import utf8, _unicode
-from tornado.util import (import_object, ObjectDict, raise_exc_info,
-                          unicode_type, _websocket_mask, re_unescape, PY3)
-from tornado.httputil import split_host_and_port
+from tornado.routing import (AnyMatches, DefaultHostMatches, HostMatches,
+                             ReversibleRouter, Rule, ReversibleRuleRouter,
+                             URLSpec)
+from tornado.util import (ObjectDict, raise_exc_info,
+                          unicode_type, _websocket_mask, PY3)
+
+url = URLSpec
 
 if PY3:
     import http.cookies as Cookie
@@ -1670,9 +1675,6 @@ def stream_request_body(cls):
     There is a subtle interaction between ``data_received`` and asynchronous
     ``prepare``: The first call to ``data_received`` may occur at any point
     after the call to ``prepare`` has returned *or yielded*.
-
-    See the `file receiver demo <https://github.com/tornadoweb/tornado/tree/master/demos/file_upload/>`_
-    for example usage.
     """
     if not issubclass(cls, RequestHandler):
         raise TypeError("expected subclass of RequestHandler, got %r", cls)
@@ -1730,7 +1732,38 @@ def addslash(method):
     return wrapper
 
 
-class Application(httputil.HTTPServerConnectionDelegate):
+class _ApplicationRouter(ReversibleRuleRouter):
+    """Routing implementation used by `Application`.
+
+    Provides a binding between `Application` and `RequestHandler` implementations.
+    This implementation extends `~.routing.ReversibleRuleRouter` in a couple of ways:
+        * it allows to use `RequestHandler` subclasses as `~.routing.Rule` target and
+        * it allows to use a list/tuple of rules as `~.routing.Rule` target. This list is
+        substituted with an `ApplicationRouter`, instantiated with current application and
+        the list of routes.
+    """
+
+    def __init__(self, application, rules=None):
+        assert isinstance(application, Application)
+        self.application = application
+        super(_ApplicationRouter, self).__init__(rules)
+
+    def process_rule(self, rule):
+        rule = super(_ApplicationRouter, self).process_rule(rule)
+
+        if isinstance(rule.target, (list, tuple)):
+            rule.target = _ApplicationRouter(self.application, rule.target)
+
+        return rule
+
+    def get_target_delegate(self, target, request, **target_params):
+        if isclass(target) and issubclass(target, RequestHandler):
+            return self.application.get_handler_delegate(request, target, **target_params)
+
+        return super(_ApplicationRouter, self).get_target_delegate(target, request, **target_params)
+
+
+class Application(ReversibleRouter):
     """A collection of request handlers that make up a web application.
 
     Instances of this class are callable and can be passed directly to
@@ -1743,20 +1776,35 @@ class Application(httputil.HTTPServerConnectionDelegate):
         http_server.listen(8080)
         ioloop.IOLoop.current().start()
 
-    The constructor for this class takes in a list of `URLSpec` objects
-    or (regexp, request_class) tuples. When we receive requests, we
-    iterate over the list in order and instantiate an instance of the
-    first request class whose regexp matches the request path.
-    The request class can be specified as either a class object or a
-    (fully-qualified) name.
+    The constructor for this class takes in a list of `~.routing.Rule`
+    objects or tuples of values corresponding to the arguments of
+    `~.routing.Rule` constructor: ``(matcher, target, [target_kwargs], [name])``,
+    the values in square brackets being optional. The default matcher is
+    `~.routing.PathMatches`, so ``(regexp, target)`` tuples can also be used
+    instead of ``(PathMatches(regexp), target)``.
+
+    A common routing target is a `RequestHandler` subclass, but you can also
+    use lists of rules as a target, which create a nested routing configuration::
 
-    Each tuple can contain additional elements, which correspond to the
-    arguments to the `URLSpec` constructor.  (Prior to Tornado 3.2,
-    only tuples of two or three elements were allowed).
+        application = web.Application([
+            (HostMatches("example.com"), [
+                (r"/", MainPageHandler),
+                (r"/feed", FeedHandler),
+            ]),
+        ])
 
-    A dictionary may be passed as the third element of the tuple,
-    which will be used as keyword arguments to the handler's
-    constructor and `~RequestHandler.initialize` method.  This pattern
+    In addition to this you can use nested `~.routing.Router` instances,
+    `~.httputil.HTTPMessageDelegate` subclasses and callables as routing targets
+    (see `~.routing` module docs for more information).
+
+    When we receive requests, we iterate over the list in order and
+    instantiate an instance of the first request class whose regexp
+    matches the request path. The request class can be specified as
+    either a class object or a (fully-qualified) name.
+
+    A dictionary may be passed as the third element (``target_kwargs``)
+    of the tuple, which will be used as keyword arguments to the handler's
+    constructor and `~RequestHandler.initialize` method. This pattern
     is used for the `StaticFileHandler` in this example (note that a
     `StaticFileHandler` can be installed automatically with the
     static_path setting described below)::
@@ -1784,7 +1832,7 @@ class Application(httputil.HTTPServerConnectionDelegate):
     ``static_handler_class`` setting.
 
     """
-    def __init__(self, handlers=None, default_host="", transforms=None,
+    def __init__(self, handlers=None, default_host=None, transforms=None,
                  **settings):
         if transforms is None:
             self.transforms = []
@@ -1792,8 +1840,6 @@ class Application(httputil.HTTPServerConnectionDelegate):
                 self.transforms.append(GZipContentEncoding)
         else:
             self.transforms = transforms
-        self.handlers = []
-        self.named_handlers = {}
         self.default_host = default_host
         self.settings = settings
         self.ui_modules = {'linkify': _linkify,
@@ -1816,8 +1862,6 @@ class Application(httputil.HTTPServerConnectionDelegate):
                             r"/(favicon\.ico)", r"/(robots\.txt)"]:
                 handlers.insert(0, (pattern, static_handler_class,
                                     static_handler_args))
-        if handlers:
-            self.add_handlers(".*$", handlers)
 
         if self.settings.get('debug'):
             self.settings.setdefault('autoreload', True)
@@ -1825,6 +1869,11 @@ class Application(httputil.HTTPServerConnectionDelegate):
             self.settings.setdefault('static_hash_cache', False)
             self.settings.setdefault('serve_traceback', True)
 
+        self.wildcard_router = _ApplicationRouter(self, handlers)
+        self.default_router = _ApplicationRouter(self, [
+            Rule(AnyMatches(), self.wildcard_router)
+        ])
+
         # Automatically reload modified modules
         if self.settings.get('autoreload'):
             from tornado import autoreload
@@ -1862,47 +1911,20 @@ class Application(httputil.HTTPServerConnectionDelegate):
         Host patterns are processed sequentially in the order they were
         added. All matching patterns will be considered.
         """
-        if not host_pattern.endswith("$"):
-            host_pattern += "$"
-        handlers = []
-        # The handlers with the wildcard host_pattern are a special
-        # case - they're added in the constructor but should have lower
-        # precedence than the more-precise handlers added later.
-        # If a wildcard handler group exists, it should always be last
-        # in the list, so insert new groups just before it.
-        if self.handlers and self.handlers[-1][0].pattern == '.*$':
-            self.handlers.insert(-1, (re.compile(host_pattern), handlers))
-        else:
-            self.handlers.append((re.compile(host_pattern), handlers))
-
-        for spec in host_handlers:
-            if isinstance(spec, (tuple, list)):
-                assert len(spec) in (2, 3, 4)
-                spec = URLSpec(*spec)
-            handlers.append(spec)
-            if spec.name:
-                if spec.name in self.named_handlers:
-                    app_log.warning(
-                        "Multiple handlers named %s; replacing previous value",
-                        spec.name)
-                self.named_handlers[spec.name] = spec
+        host_matcher = HostMatches(host_pattern)
+        rule = Rule(host_matcher, _ApplicationRouter(self, host_handlers))
+
+        self.default_router.rules.insert(-1, rule)
+
+        if self.default_host is not None:
+            self.wildcard_router.add_rules([(
+                DefaultHostMatches(self, host_matcher.host_pattern),
+                host_handlers
+            )])
 
     def add_transform(self, transform_class):
         self.transforms.append(transform_class)
 
-    def _get_host_handlers(self, request):
-        host = split_host_and_port(request.host.lower())[0]
-        matches = []
-        for pattern, handlers in self.handlers:
-            if pattern.match(host):
-                matches.extend(handlers)
-        # Look for default host if not behind load balancer (for debugging)
-        if not matches and "X-Real-Ip" not in request.headers:
-            for pattern, handlers in self.handlers:
-                if pattern.match(self.default_host):
-                    matches.extend(handlers)
-        return matches or None
-
     def _load_ui_methods(self, methods):
         if isinstance(methods, types.ModuleType):
             self._load_ui_methods(dict((n, getattr(methods, n))
@@ -1932,16 +1954,30 @@ class Application(httputil.HTTPServerConnectionDelegate):
                 except TypeError:
                     pass
 
-    def start_request(self, server_conn, request_conn):
-        # Modern HTTPServer interface
-        return _RequestDispatcher(self, request_conn)
-
     def __call__(self, request):
         # Legacy HTTPServer interface
-        dispatcher = _RequestDispatcher(self, None)
-        dispatcher.set_request(request)
+        dispatcher = self.find_handler(request)
         return dispatcher.execute()
 
+    def find_handler(self, request, **kwargs):
+        route = self.default_router.find_handler(request)
+        if route is not None:
+            return route
+
+        if self.settings.get('default_handler_class'):
+            return self.get_handler_delegate(
+                request,
+                self.settings['default_handler_class'],
+                self.settings.get('default_handler_args', {}))
+
+        return self.get_handler_delegate(
+            request, ErrorHandler, {'status_code': 404})
+
+    def get_handler_delegate(self, request, target_class, target_kwargs=None,
+                             path_args=None, path_kwargs=None):
+        return _HandlerDelegate(
+            self, request, target_class, target_kwargs, path_args, path_kwargs)
+
     def reverse_url(self, name, *args):
         """Returns a URL path for handler named ``name``
 
@@ -1951,8 +1987,10 @@ class Application(httputil.HTTPServerConnectionDelegate):
         They will be converted to strings if necessary, encoded as utf8,
         and url-escaped.
         """
-        if name in self.named_handlers:
-            return self.named_handlers[name].reverse(*args)
+        reversed_url = self.default_router.reverse_url(name, *args)
+        if reversed_url is not None:
+            return reversed_url
+
         raise KeyError("%s not found in named urls" % name)
 
     def log_request(self, handler):
@@ -1977,67 +2015,24 @@ class Application(httputil.HTTPServerConnectionDelegate):
                    handler._request_summary(), request_time)
 
 
-class _RequestDispatcher(httputil.HTTPMessageDelegate):
-    def __init__(self, application, connection):
+class _HandlerDelegate(httputil.HTTPMessageDelegate):
+    def __init__(self, application, request, handler_class, handler_kwargs,
+                 path_args, path_kwargs):
         self.application = application
-        self.connection = connection
-        self.request = None
+        self.connection = request.connection
+        self.request = request
+        self.handler_class = handler_class
+        self.handler_kwargs = handler_kwargs or {}
+        self.path_args = path_args or []
+        self.path_kwargs = path_kwargs or {}
         self.chunks = []
-        self.handler_class = None
-        self.handler_kwargs = None
-        self.path_args = []
-        self.path_kwargs = {}
+        self.stream_request_body = _has_stream_request_body(self.handler_class)
 
     def headers_received(self, start_line, headers):
-        self.set_request(httputil.HTTPServerRequest(
-            connection=self.connection, start_line=start_line,
-            headers=headers))
         if self.stream_request_body:
             self.request.body = Future()
             return self.execute()
 
-    def set_request(self, request):
-        self.request = request
-        self._find_handler()
-        self.stream_request_body = _has_stream_request_body(self.handler_class)
-
-    def _find_handler(self):
-        # Identify the handler to use as soon as we have the request.
-        # Save url path arguments for later.
-        app = self.application
-        handlers = app._get_host_handlers(self.request)
-        if not handlers:
-            self.handler_class = RedirectHandler
-            self.handler_kwargs = dict(url="%s://%s/"
-                                       % (self.request.protocol,
-                                          app.default_host))
-            return
-        for spec in handlers:
-            match = spec.regex.match(self.request.path)
-            if match:
-                self.handler_class = spec.handler_class
-                self.handler_kwargs = spec.kwargs
-                if spec.regex.groups:
-                    # Pass matched groups to the handler.  Since
-                    # match.groups() includes both named and
-                    # unnamed groups, we want to use either groups
-                    # or groupdict but not both.
-                    if spec.regex.groupindex:
-                        self.path_kwargs = dict(
-                            (str(k), _unquote_or_none(v))
-                            for (k, v) in match.groupdict().items())
-                    else:
-                        self.path_args = [_unquote_or_none(s)
-                                          for s in match.groups()]
-                return
-        if app.settings.get('default_handler_class'):
-            self.handler_class = app.settings['default_handler_class']
-            self.handler_kwargs = app.settings.get(
-                'default_handler_args', {})
-        else:
-            self.handler_class = ErrorHandler
-            self.handler_kwargs = dict(status_code=404)
-
     def data_received(self, data):
         if self.stream_request_body:
             return self.handler.data_received(data)
@@ -3012,99 +3007,6 @@ class _UIModuleNamespace(object):
             raise AttributeError(str(e))
 
 
-class URLSpec(object):
-    """Specifies mappings between URLs and handlers."""
-    def __init__(self, pattern, handler, kwargs=None, name=None):
-        """Parameters:
-
-        * ``pattern``: Regular expression to be matched. Any capturing
-          groups in the regex will be passed in to the handler's
-          get/post/etc methods as arguments (by keyword if named, by
-          position if unnamed. Named and unnamed capturing groups may
-          may not be mixed in the same rule).
-
-        * ``handler``: `RequestHandler` subclass to be invoked.
-
-        * ``kwargs`` (optional): A dictionary of additional arguments
-          to be passed to the handler's constructor.
-
-        * ``name`` (optional): A name for this handler.  Used by
-          `Application.reverse_url`.
-
-        """
-        if not pattern.endswith('$'):
-            pattern += '$'
-        self.regex = re.compile(pattern)
-        assert len(self.regex.groupindex) in (0, self.regex.groups), \
-            ("groups in url regexes must either be all named or all "
-             "positional: %r" % self.regex.pattern)
-
-        if isinstance(handler, str):
-            # import the Module and instantiate the class
-            # Must be a fully qualified name (module.ClassName)
-            handler = import_object(handler)
-
-        self.handler_class = handler
-        self.kwargs = kwargs or {}
-        self.name = name
-        self._path, self._group_count = self._find_groups()
-
-    def __repr__(self):
-        return '%s(%r, %s, kwargs=%r, name=%r)' % \
-            (self.__class__.__name__, self.regex.pattern,
-             self.handler_class, self.kwargs, self.name)
-
-    def _find_groups(self):
-        """Returns a tuple (reverse string, group count) for a url.
-
-        For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
-        would return ('/%s/%s/', 2).
-        """
-        pattern = self.regex.pattern
-        if pattern.startswith('^'):
-            pattern = pattern[1:]
-        if pattern.endswith('$'):
-            pattern = pattern[:-1]
-
-        if self.regex.groups != pattern.count('('):
-            # The pattern is too complicated for our simplistic matching,
-            # so we can't support reversing it.
-            return (None, None)
-
-        pieces = []
-        for fragment in pattern.split('('):
-            if ')' in fragment:
-                paren_loc = fragment.index(')')
-                if paren_loc >= 0:
-                    pieces.append('%s' + fragment[paren_loc + 1:])
-            else:
-                try:
-                    unescaped_fragment = re_unescape(fragment)
-                except ValueError as exc:
-                    # If we can't unescape part of it, we can't
-                    # reverse this url.
-                    return (None, None)
-                pieces.append(unescaped_fragment)
-
-        return (''.join(pieces), self.regex.groups)
-
-    def reverse(self, *args):
-        if self._path is None:
-            raise ValueError("Cannot reverse url regex " + self.regex.pattern)
-        assert len(args) == self._group_count, "required number of arguments "\
-            "not found"
-        if not len(args):
-            return self._path
-        converted_args = []
-        for a in args:
-            if not isinstance(a, (unicode_type, bytes)):
-                a = str(a)
-            converted_args.append(escape.url_escape(utf8(a), plus=False))
-        return self._path % tuple(converted_args)
-
-url = URLSpec
-
-
 if hasattr(hmac, 'compare_digest'):  # python 3.3
     _time_independent_equals = hmac.compare_digest
 else:
@@ -3325,15 +3227,3 @@ def _create_signature_v2(secret, s):
     hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
     hash.update(utf8(s))
     return utf8(hash.hexdigest())
-
-
-def _unquote_or_none(s):
-    """None-safe wrapper around url_unescape to handle unmatched optional
-    groups correctly.
-
-    Note that args are passed as bytes so the handler can decide what
-    encoding to use.
-    """
-    if s is None:
-        return s
-    return escape.url_unescape(s, encoding=None, plus=False)


Test patch:
diff --git a/tornado/test/concurrent_test.py b/tornado/test/concurrent_test.py
index 4e5118b1..8ce095ec 100644
--- a/tornado/test/concurrent_test.py
+++ b/tornado/test/concurrent_test.py
@@ -15,7 +15,6 @@
 # under the License.
 from __future__ import absolute_import, division, print_function, with_statement
 
-import gc
 import logging
 import re
 import socket
@@ -26,10 +25,9 @@ from tornado.concurrent import Future, return_future, ReturnValueIgnoredError, r
 from tornado.escape import utf8, to_unicode
 from tornado import gen
 from tornado.iostream import IOStream
-from tornado.log import app_log
 from tornado import stack_context
 from tornado.tcpserver import TCPServer
-from tornado.testing import AsyncTestCase, ExpectLog, LogTrapTestCase, bind_unused_port, gen_test
+from tornado.testing import AsyncTestCase, LogTrapTestCase, bind_unused_port, gen_test
 from tornado.test.util import unittest
 
 
@@ -173,24 +171,6 @@ class ReturnFutureTest(AsyncTestCase):
             tb = traceback.extract_tb(sys.exc_info()[2])
             self.assertIn(self.expected_frame, tb)
 
-    @gen_test
-    def test_uncaught_exception_log(self):
-        @gen.coroutine
-        def f():
-            yield gen.moment
-            1/0
-
-        g = f()
-
-        with ExpectLog(app_log,
-                       "(?s)Future.* exception was never retrieved:"
-                       ".*ZeroDivisionError"):
-            yield gen.moment
-            yield gen.moment
-            del g
-            gc.collect()  # for PyPy
-
-
 # The following series of classes demonstrate and test various styles
 # of use, with and without generators and futures.
 
diff --git a/tornado/test/gen_test.py b/tornado/test/gen_test.py
index 8bbfc5fa..bfaba567 100644
--- a/tornado/test/gen_test.py
+++ b/tornado/test/gen_test.py
@@ -657,28 +657,6 @@ class GenCoroutineTest(AsyncTestCase):
         super(GenCoroutineTest, self).tearDown()
         assert self.finished
 
-    def test_attributes(self):
-        self.finished = True
-
-        def f():
-            yield gen.moment
-
-        coro = gen.coroutine(f)
-        self.assertEqual(coro.__name__, f.__name__)
-        self.assertEqual(coro.__module__, f.__module__)
-        self.assertIs(coro.__wrapped__, f)
-
-    def test_is_coroutine_function(self):
-        self.finished = True
-
-        def f():
-            yield gen.moment
-
-        coro = gen.coroutine(f)
-        self.assertFalse(gen.is_coroutine_function(f))
-        self.assertTrue(gen.is_coroutine_function(coro))
-        self.assertFalse(gen.is_coroutine_function(coro()))
-
     @gen_test
     def test_sync_gen_return(self):
         @gen.coroutine
diff --git a/tornado/test/routing_test.py b/tornado/test/routing_test.py
new file mode 100644
index 00000000..37f7cfd3
--- /dev/null
+++ b/tornado/test/routing_test.py
@@ -0,0 +1,158 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+from __future__ import absolute_import, division, print_function, with_statement
+
+from tornado.httputil import HTTPHeaders, HTTPMessageDelegate, HTTPServerConnectionDelegate, ResponseStartLine
+from tornado.routing import HostMatches, PathMatches, ReversibleRouter, Rule, RuleRouter
+from tornado.testing import AsyncHTTPTestCase
+from tornado.web import Application, RequestHandler
+from tornado.wsgi import WSGIContainer
+
+
+def get_named_handler(handler_name):
+    class Handler(RequestHandler):
+        def get(self, *args, **kwargs):
+            if self.application.settings.get("app_name") is not None:
+                self.write(self.application.settings["app_name"] + ": ")
+
+            self.finish(handler_name + ": " + self.reverse_url(handler_name))
+
+    return Handler
+
+
+FirstHandler = get_named_handler("first_handler")
+SecondHandler = get_named_handler("second_handler")
+
+
+class CustomRouter(ReversibleRouter):
+    def __init__(self):
+        super(CustomRouter, self).__init__()
+        self.routes = {}
+
+    def add_routes(self, routes):
+        self.routes.update(routes)
+
+    def find_handler(self, request, **kwargs):
+        if request.path in self.routes:
+            app, handler = self.routes[request.path]
+            return app.get_handler_delegate(request, handler)
+
+    def reverse_url(self, name, *args):
+        handler_path = '/' + name
+        return handler_path if handler_path in self.routes else None
+
+
+class CustomRouterTestCase(AsyncHTTPTestCase):
+    def get_app(self):
+        class CustomApplication(Application):
+            def reverse_url(self, name, *args):
+                return router.reverse_url(name, *args)
+
+        router = CustomRouter()
+        app1 = CustomApplication(app_name="app1")
+        app2 = CustomApplication(app_name="app2")
+
+        router.add_routes({
+            "/first_handler": (app1, FirstHandler),
+            "/second_handler": (app2, SecondHandler),
+            "/first_handler_second_app": (app2, FirstHandler),
+        })
+
+        return router
+
+    def test_custom_router(self):
+        response = self.fetch("/first_handler")
+        self.assertEqual(response.body, b"app1: first_handler: /first_handler")
+        response = self.fetch("/second_handler")
+        self.assertEqual(response.body, b"app2: second_handler: /second_handler")
+        response = self.fetch("/first_handler_second_app")
+        self.assertEqual(response.body, b"app2: first_handler: /first_handler")
+
+
+class ConnectionDelegate(HTTPServerConnectionDelegate):
+    def start_request(self, server_conn, request_conn):
+
+        class MessageDelegate(HTTPMessageDelegate):
+            def __init__(self, connection):
+                self.connection = connection
+
+            def finish(self):
+                response_body = b"OK"
+                self.connection.write_headers(
+                    ResponseStartLine("HTTP/1.1", 200, "OK"),
+                    HTTPHeaders({"Content-Length": str(len(response_body))}))
+                self.connection.write(response_body)
+                self.connection.finish()
+
+        return MessageDelegate(request_conn)
+
+
+class RuleRouterTest(AsyncHTTPTestCase):
+    def get_app(self):
+        app = Application()
+
+        def request_callable(request):
+            request.write(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
+            request.finish()
+
+        app.add_handlers(".*", [
+            (HostMatches("www.example.com"), [
+                (PathMatches("/first_handler"), "tornado.test.routing_test.SecondHandler", {}, "second_handler")
+            ]),
+            Rule(PathMatches("/first_handler"), FirstHandler, name="first_handler"),
+            Rule(PathMatches("/request_callable"), request_callable),
+            ("/connection_delegate", ConnectionDelegate())
+        ])
+
+        return app
+
+    def test_rule_based_router(self):
+        response = self.fetch("/first_handler")
+        self.assertEqual(response.body, b"first_handler: /first_handler")
+        response = self.fetch("/first_handler", headers={'Host': 'www.example.com'})
+        self.assertEqual(response.body, b"second_handler: /first_handler")
+
+        response = self.fetch("/connection_delegate")
+        self.assertEqual(response.body, b"OK")
+
+        response = self.fetch("/request_callable")
+        self.assertEqual(response.body, b"OK")
+
+        response = self.fetch("/404")
+        self.assertEqual(response.code, 404)
+
+
+class WSGIContainerTestCase(AsyncHTTPTestCase):
+    def get_app(self):
+        wsgi_app = WSGIContainer(self.wsgi_app)
+
+        class Handler(RequestHandler):
+            def get(self, *args, **kwargs):
+                self.finish(self.reverse_url("tornado"))
+
+        return RuleRouter([
+            (PathMatches("/tornado.*"), Application([(r"/tornado/test", Handler, {}, "tornado")])),
+            (PathMatches("/wsgi"), wsgi_app),
+        ])
+
+    def wsgi_app(self, environ, start_response):
+        start_response("200 OK", [])
+        return [b"WSGI"]
+
+    def test_wsgi_container(self):
+        response = self.fetch("/tornado/test")
+        self.assertEqual(response.body, b"/tornado/test")
+
+        response = self.fetch("/wsgi")
+        self.assertEqual(response.body, b"WSGI")
diff --git a/tornado/test/runtests.py b/tornado/test/runtests.py
index 1d6199f3..5dc4abe6 100644
--- a/tornado/test/runtests.py
+++ b/tornado/test/runtests.py
@@ -43,6 +43,7 @@ TEST_MODULES = [
     'tornado.test.options_test',
     'tornado.test.process_test',
     'tornado.test.queues_test',
+    'tornado.test.routing_test',
     'tornado.test.simple_httpclient_test',
     'tornado.test.stack_context_test',
     'tornado.test.tcpclient_test',
@@ -125,9 +126,6 @@ def main():
     # Silence the warning until we can drop 3.5.[01].
     warnings.filterwarnings("ignore", category=PendingDeprecationWarning,
                             message=".*legacy __aiter__ protocol")
-    # 3.5.2's PendingDeprecationWarning became a DeprecationWarning in 3.6.
-    warnings.filterwarnings("ignore", category=DeprecationWarning,
-                            message=".*legacy __aiter__ protocol")
 
     logging.getLogger("tornado.access").setLevel(logging.CRITICAL)
 
diff --git a/tornado/test/template_test.py b/tornado/test/template_test.py
index 176d41a7..dfcf3805 100644
--- a/tornado/test/template_test.py
+++ b/tornado/test/template_test.py
@@ -6,8 +6,8 @@ import traceback
 
 from tornado.escape import utf8, native_str, to_unicode
 from tornado.template import Template, DictLoader, ParseError, Loader
-from tornado.test.util import unittest, is_coverage_running
-from tornado.util import ObjectDict, unicode_type, PY3
+from tornado.test.util import unittest
+from tornado.util import ObjectDict, unicode_type
 
 
 class TemplateTest(unittest.TestCase):
@@ -175,11 +175,6 @@ try{% set y = 1/x %}
         self.assertEqual(template.generate(), '0')
 
     def test_non_ascii_name(self):
-        if PY3 and is_coverage_running():
-            try:
-                os.fsencode(u"t\u00e9st.html")
-            except UnicodeEncodeError:
-                self.skipTest("coverage tries to access unencodable filename")
         loader = DictLoader({u"t\u00e9st.html": "hello"})
         self.assertEqual(loader.load(u"t\u00e9st.html").generate(), b"hello")
 
diff --git a/tornado/test/util.py b/tornado/test/util.py
index 429bc024..2e3d779f 100644
--- a/tornado/test/util.py
+++ b/tornado/test/util.py
@@ -76,21 +76,3 @@ def exec_test(caller_globals, caller_locals, s):
     local_namespace = {}
     exec(textwrap.dedent(s), global_namespace, local_namespace)
     return local_namespace
-
-
-def is_coverage_running():
-    """Return whether coverage is currently running.
-    """
-    if 'coverage' not in sys.modules:
-        return False
-    tracer = sys.gettrace()
-    if tracer is None:
-        return False
-    try:
-        mod = tracer.__module__
-    except AttributeError:
-        try:
-            mod = tracer.__class__.__module__
-        except AttributeError:
-            return False
-    return mod.startswith('coverage')
diff --git a/tornado/test/util_test.py b/tornado/test/util_test.py
index cbb89322..48b16f89 100644
--- a/tornado/test/util_test.py
+++ b/tornado/test/util_test.py
@@ -6,7 +6,7 @@ import datetime
 
 import tornado.escape
 from tornado.escape import utf8
-from tornado.util import raise_exc_info, Configurable, exec_in, ArgReplacer, timedelta_to_seconds, import_object, re_unescape, is_finalizing, PY3
+from tornado.util import raise_exc_info, Configurable, exec_in, ArgReplacer, timedelta_to_seconds, import_object, re_unescape, PY3
 from tornado.test.util import unittest
 
 if PY3:
@@ -220,8 +220,3 @@ class ReUnescapeTest(unittest.TestCase):
             re_unescape('\\b')
         with self.assertRaises(ValueError):
             re_unescape('\\Z')
-
-
-class IsFinalizingTest(unittest.TestCase):
-    def test_basic(self):
-        self.assertFalse(is_finalizing())
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
index b09bb9ab..ff722c65 100644
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -1348,6 +1348,8 @@ class HostMatchingTest(WebTestCase):
                               [("/bar", HostMatchingTest.Handler, {"reply": "[1]"})])
         self.app.add_handlers("www.example.com",
                               [("/baz", HostMatchingTest.Handler, {"reply": "[2]"})])
+        self.app.add_handlers("www.e.*e.com",
+                              [("/baz", HostMatchingTest.Handler, {"reply": "[3]"})])
 
         response = self.fetch("/foo")
         self.assertEqual(response.body, b"wildcard")
@@ -1362,6 +1364,8 @@ class HostMatchingTest(WebTestCase):
         self.assertEqual(response.body, b"[1]")
         response = self.fetch("/baz", headers={'Host': 'www.example.com'})
         self.assertEqual(response.body, b"[2]")
+        response = self.fetch("/baz", headers={'Host': 'www.exe.com'})
+        self.assertEqual(response.body, b"[3]")
 
 
 @wsgi_safe
@@ -1387,8 +1391,8 @@ class DefaultHostMatchingTest(WebTestCase):
         response = self.fetch("/baz")
         self.assertEqual(response.code, 404)
 
-        response = self.fetch("/foo", follow_redirects=False, headers={"X-Real-Ip": "127.0.0.1"})
-        self.assertEqual(response.code, 301)
+        response = self.fetch("/foo", headers={"X-Real-Ip": "127.0.0.1"})
+        self.assertEqual(response.code, 404)
 
         self.app.default_host = "www.test.com"