Repository: Jaymon/endpoints (language: python)

Issue:
endpoints client server is needlessly complex
The wsgi clients use a subprocess and thread to run the server using the `endpoints.__main__` module, this seems excessive, why not just run it in a thread locally? I think that would be simpler and easier, the uwsgi makes a bit more sense since it runs uwsgi, but even there, it might be worth looking at the new testdata.command stuff that uses subprocess and threading since that codes is newer and conceptually simpler


Discussion / hints:
the reason the server clients use a subprocess is for things like uwsgi which is ran outside of python, and it's just easier to make them all use subprocess, even if they just call `python -m endpoints` to keep it consistent.

It would be worth going through the subprocess code and updating it to testdata's standard though since that is the best subprocess code I've got right now. I actually removed the client code from captain recently because it wasn't worth having testdata like code in 2 places, but I think endpoints is different since each server needs to be customized and so trying to wrap testdata would result in almost the same amount of code anyway

Fix patch:
diff --git a/README.md b/README.md
index f34a49b..cd4a48e 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 Quickest API builder in the West! 
 
-_Endpoints_ is a lightweight REST api framework written in python and used in multiple production systems that handle millions of requests daily.
+_Endpoints_ is a lightweight REST api framework written in python that supports both WSGI and ASGI. _Endpoints_ has been used in multiple production systems that handle millions of requests daily.
 
 
 ## 5 Minute Getting Started
@@ -17,12 +17,6 @@ If you want the latest and greatest you can also install from source:
 
     $ pip install -U "git+https://github.com/jaymon/endpoints#egg=endpoints"
 
-**Note:** if you get the following error
-
-    $ pip: command not found
-
-you will need to [install pip](https://pip.pypa.io/en/stable/installing/).
-
 
 ### Set Up Your Controller File
 
@@ -36,25 +30,31 @@ Add the following code to your new Controller file. These classes are examples o
 from endpoints import Controller
 
 class Default(Controller):
-  def GET(self):
+  async def GET(self):
     return "boom"
 
-  def POST(self, **kwargs):
+  async def POST(self, **kwargs):
     return 'hello {}'.format(kwargs['name'])
 
 class Foo(Controller):
-  def GET(self):
+  async def GET(self):
     return "bang"
 ```
 
 
-### Start a Server
+### Start a WSGI Server
 
 Now that you have your `controllers.py`, let's use the built-in WSGI server to serve them, we'll set our `controllers.py` file as the [controller prefix](docs/PREFIXES.md) so Endpoints will know where to find the [Controller classes](docs/CONTROLLERS.md) we just defined:
 
     $ endpoints --prefix=controllers --host=localhost:8000
 
 
+### Start an ASGI Server
+
+Install [Daphne](https://github.com/django/daphne) and start it:
+
+    $ ENDPOINTS_PREFIX=controllers daphne -b localhost -p 8000 -v 3 endpoints.interface.asgi.ApplicationFactory
+
 ### Test it out
 
 Using curl:
@@ -122,11 +122,11 @@ and the file `controllers/__init__.py` contained:
 from endpoints import Controller
 
 class Default(Controller):
-  def GET(self):
+  async def GET(self):
     return "called /"
 
 class Foo(Controller):
-  def GET(self):
+  async def GET(self):
     return "called /foo"
 ```
 
diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md
index 07be7d8..24f1074 100644
--- a/docs/AUTHENTICATION.md
+++ b/docs/AUTHENTICATION.md
@@ -5,22 +5,22 @@ Validating access to certain resources is pretty common and so _endpoints_ provi
 
 ## Http basic authentication
 
-Want to have the browser prompt for a username and password? Use the `endpoints.decorators.auth.basic_auth` decorator:
+Want to have the browser prompt for a username and password? Use the `endpoints.decorators.auth.auth_basic` decorator:
 
 ```python
 # controller.py
 # create a basic http auth decorator
 
 from endpoints import Controller
-from endpoints.decorators.auth import basic_auth
-
-def target(request, username, password):
-    return username == "foo" and password == "bar"
+from endpoints.decorators.auth import AuthBackend, auth_basic
 
+class Backend(AuthBackend):
+    async def auth_basic(self, controller, username, password):
+        return username == "foo" and password == "bar"
 
 class Default(Controller):
-    @basic_auth(target=target)
-    def GET(self):
+    @basic_auth(backend_class=Backend)
+    async def GET(self):
         return "hello world"
 ```
 
@@ -31,8 +31,8 @@ That's it, now any request to `/` will prompt for username and password if not p
 
 Check out the `endpoints.decorators.auth` module for other authentication decorators
 
-* `client_auth` - Similar to `basic_auth` but checks for **client_id** and **client_secret** instead of username and password.
-* `token_auth` - Checks for an `Authorization  Bearer <TOKEN>` header.
+* `auth_client` - Similar to `auth_basic` but checks for **client_id** and **client_secret** instead of username and password.
+* `auth_token` - Checks for an `Authorization  Bearer <TOKEN>` header.
 
 
 ## Customization
@@ -40,32 +40,34 @@ Check out the `endpoints.decorators.auth` module for other authentication decora
 You can extend any of the auth decorators to fit them into your own system:
 
 ```python
-from endpoints.decorators.auth import basic_auth
-
-class auth_user(basic_auth):
-    """validate a user in our system and set request.user if a valid user is found"""
+from endpoints.decorators.auth import AuthBackend, auth_basic
 
-    def target(self, request, username, password):
+class Backend(AuthBackend):
+    async def auth_user(self, controller, username, password):
         # validate username and password using app specific db or whatnot
         user = magical_db_check(username, password)
         request.user = user
         return True # true if user is valid, false otherwise
 
-    def decorate(self, f):
-        # get rid of letting the decorator take a target keyword since we use self.target
-        return super(auth_user, self).decorate(f, target=None)
+class auth_user(auth_basic):
+    """validate a user in our system and set request.user if a valid user is found"""
+    backend_class = Backend
 ``` 
 
-But there is an `AuthDecorator` specifically designed for easy overriding for general purpose authentication, it expects that the child class will implement a `target` method that takes same params as a controller and, also like a controller, request is available through the instance property `self.request`.
+There is also an `AuthDecorator` specifically designed for easy overriding for general purpose authentication:
 
 ```python
 from endpoints import Controller
-from endpoints.decorators.auth import AuthDecorator
-
+from endpoints.decorators.auth import AuthBackend, AuthDecorator
+
+class Backend(AuthBackend):
+    async def auth(self, controller, controller_args, controller_kwargs):
+        # check something or do something to validate the request
+        # return True if auth was valid, False otherwise
+        
+    
 class auth(AuthDecorator):
-    def target(self, *args, **kwargs):
-        if kwargs["key"] != foo
-            raise ValueError("invalid access token")
+    backend_class = Backend
 
 
 class Default(Controller):
@@ -81,90 +83,52 @@ Sometimes, you might only want certain users to be able to access certain endpoi
 
 
 ```python
-from endpoints.decorators.auth import AuthDecorator
-
+from endpoints.decorators.auth import AuthBackend, AuthDecorator
 
-class PermAuth(AuthDecorator):
+class Backend(AuthBackend):
+    async def perm_auth(self, user_perms, valid_perms):
+        return len(user_perms.intersection(valid_perms)) > 0
 
-    def decorate(self, func, *perms):
+class perm_auth(AuthDecorator):
+    backend_class = Backend
+    
+    def definition(self, *perms):
         self.perms = perms
-        return super(PermAuth, self).decorate(func)
 
-    def normalize_target_params(self, request, controller_args, controller_kwargs):
-        user = get_user(request)
-        return [], {
+    async def handle_kwargs(self, controller, controller_args, controller_kwargs):
+        user = await get_user(request)
+        return {
+            'method_name': "perm_auth",
             'user_perms': set(user.perms),
             'valid_perms': set(self.perms),
         }
-
-    def target(self, request, user, user_perms, valid_perms):
-        return len(user_perms.intersection(valid_perms)) > 0
 ```
 
-First, we setup our `PermAuth` decorator to accept one or more permissions, we do this by overriding the `decorate` method and saving those passed in permissions for later use:
+First, we setup our `PermAuth` decorator to accept one or more permissions, we do this by overriding the `definition` method and saving those passed in permissions for later use:
 
 ```python
-def decorate(self, func, *perms):
+def definition(self, *perms):
     self.perms = perms
 ```
 
-Next, we override `normalize_target_params` to setup the params that will be sent to our `target` method, this method returns a tuple `list, dict` that will get sent to our target method in the form of: `*args, **kwargs`.
+Next, we override `handle_kwargs` to setup the params that will be sent to our `Backend.auth` method, this method returns a `dict` that will get sent to our `Backend.auth` method in the form of: `**kwargs`.
 
-In this instance, our `normalize_target_params` pulls our user out from some magical `get_user` and then returns that user's permissions along with our saved permissions from the `decorate` method.
+In this instance, our `handle_kwargs` pulls our user out from some magical async `get_user` method and then returns that user's permissions along with our saved permissions set in the `definition` method.
 
-And finally, we add our `target` method to check the values `normalize_target_params` gave us.
+And finally, we add our `Backend.auth` method to check the values `handle_kwargs` gave us.
 
 Now, we can use this decorator on our Controller methods:
 
 ```python
 from endpoints import Controller
 
-
 class Default(Controller):
-    @PermAuth("bar", "che") # bar and che can access GET
-    def GET(self):
+    @perm_auth("bar", "che") # bar and che can access GET
+    async def GET(self):
         return "user can GET\n"
 
-    @PermAuth("bar") # you must have bar perms to POST
-    def POST(self, **kwargs):
+    @perm_auth("bar") # you must have bar perms to POST
+    async def POST(self, **kwargs):
         return "user can POST\n"
 
 ```
-
-Now let's try it out, but first we'll need to flesh out our `get_user` method:
-
-```python
-
-def get_user(request):
-    class User(object):
-        def __init__(self, *perms):
-            self.perms = perms
-
-    mapping = {
-        ("alice", "1234"): User("bar"),
-        ("bob", "1234"): User("che")
-    }
-
-    return mapping[request.get_auth_basic()]
-```
-
-
-You can find the above code in `examples/authex1.py`. So let's start our server:
-
-    $ cd example
-    $ python ../endpoints/bin/wsgiserver.py --prefix authex1 --host 127.0.0.1:8080
-
-And then let's make some requests to test it out:
-
-
-    $ curl -u "alice:1234" "http://127.0.0.1:8080"
-    "user can GET"
-    $ curl -u "alice:1234" "http://127.0.0.1:8080" -X "POST"
-    "user can POST"
-    $ curl -u "bob:1234" "http://127.0.0.1:8080" -X "POST"
-    {"errmsg": "PermAuth check failed"}
-    $ curl -u "bob:1234" "http://127.0.0.1:8080"
-    "user can GET"
-
-Yay, it works as expected!
-
diff --git a/docs/CONTROLLERS.md b/docs/CONTROLLERS.md
index cc94941..aea991e 100644
--- a/docs/CONTROLLERS.md
+++ b/docs/CONTROLLERS.md
@@ -11,7 +11,7 @@ Create a file named _controllers.py_ with the following contents:
 from endpoints import Controller
 
 class Hello(Controller):
-    def GET(self):
+    async def GET(self):
         return "hello world"
 ```
 
@@ -20,7 +20,6 @@ Now you can make a request:
     $ curl http://localhost:8000/hello
     "hello world"
 
-
 Now let's try some different stuff:
 
     $ curl http://localhost:8000/hello -d "name=Alice"
@@ -33,11 +32,11 @@ from endpoints import Controller
 from endpoints.decorators import param
 
 class Hello(Controller):
-    def GET(self):
+    async def GET(self):
         return "hello world"
 
     @param("name")
-    def POST(self, **kwargs):
+    async def POST(self, **kwargs):
         return "hello {}".format(kwargs["name"])
 ```
 
@@ -52,12 +51,13 @@ Nice, but now we want to be even more clever, we want **GET** to act differently
 
 ```python
 from endpoints import Controller
-from endpoints.decorators.auth import basic_auth
+from endpoints.decorators.auth import auth_basic, AuthBackend
 from endpoints.decorators import route
 
-class auth(basic_auth):
-    def target(self, request, username, password):
+class Backend(AuthBackend):
+    async def handle(self, controller, username, password):
         ret = False
+        request = controller.request
         if username == "Alice" and password == "1234":
             request.username = "Alice"
             ret = True
@@ -66,16 +66,17 @@ class auth(basic_auth):
             ret = True
         return ret
 
+auth_basic.backend_class = Backend
 
 class Hello(Controller):
-    @auth()
+    @auth_basic()
     @route(lambda request: request.username == "Alice")
-    def GET_alice(self):
+    async def GET_alice(self):
         return "hello Alice"
 
-    @auth()
+    @auth_basic()
     @route(lambda request: request.username == "Bob")
-    def GET_bob(self, **kwargs):
+    async def GET_bob(self, **kwargs):
         return "hola Bob"
 ```
 
@@ -96,8 +97,8 @@ You can define your controller methods to accept certain path params and to acce
 
 ```python
 class Foo(Controller):
-  def GET(self, one, two=None, **params): pass
-  def POST(self, **params): pass
+  async def GET(self, one, two=None, **params): pass
+  async def POST(self, **params): pass
 ```
 
 your call requests would be translated like this:
@@ -132,7 +133,7 @@ class Default(Controller):
 
   cors = False # Turn off cors
 
-  def GET(self):
+  async def GET(self):
     return "This will not have CORS support"
 ```
 
@@ -149,10 +150,10 @@ For example:
 from endpoints import Controller
 
 class Default(Controller):
-	def GET(self, *args, **kwargs):
+	async def GET(self, *args, **kwargs):
 		return "GET {}".format("/".join(args))
 	
-	def POST(self, *args, **kwargs):
+	async def POST(self, *args, **kwargs):
 		return "POST {}".format("/".join(args))
 ```
 
@@ -172,7 +173,7 @@ This makes it possible for you to define your paths in multiple different ways,
 from endpoints import Controller
 
 class User(Controller):
-	def GET(self): pass
+	async def GET(self): pass
 ```
 
 would be equivalent to:
@@ -183,7 +184,7 @@ would be equivalent to:
 from endpoints import Controller
 
 class Default(Controller):
-	def GET(self): pass
+	async def GET(self): pass
 ```
 
 Endpoints first checks module paths, then it checks classes in the found module, then defaults to the `Default` class if no other suitable class is found.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index d71d34f..f08153c 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -2,9 +2,17 @@
 
 ## Unit Tests
 
+Install `pyt`:
+
+    $ pip install pyt
+
 After cloning the repo, `cd` into the repo's directory and run:
 
-    $ python -m unittest endpoints_test
+    $ pyt tests
+    
+You can probably run them without using `pyt` also:
+
+    $ python -m unittest tests
 
 Check the `tests_require` parameter in the `setup.py` script to see what modules are needed to run the tests because there are dependencies that the tests need that the rest of the package does not.
 
@@ -20,4 +28,3 @@ and used with endpoints like so:
     $ ls -d * | entr sh -c "killall endpoints; endpoints --prefix=mycontroller --host=localhost:8000 &"
 
 Hat tip to [Mindey](https://github.com/firstopinion/endpoints/issues/57) for this command.
-
diff --git a/docs/INTERFACES.md b/docs/INTERFACES.md
deleted file mode 100644
index 510c61e..0000000
--- a/docs/INTERFACES.md
+++ /dev/null
@@ -1,92 +0,0 @@
-# Interfaces
-
-## Creating a new web interface
-
-You can start with a template like this:
-
-```python
-from endpoints.interface import BaseServer
-
-
-class Server(BaseServer):
-    backend_class = None
-    """the supported server's interface, there is no common interface for this class.
-    Basically it is the raw backend class that the BaseServer child is translating
-    for endpoints compatibility"""
-
-    @property
-    def hostloc(self):
-        """Return host:port string that the server is using to answer requests"""
-        raise NotImplementedError()
-
-    def create_backend(self, **kwargs):
-        """create instance of the backend class.
-
-        Endpoints works by translating incoming requests from this instance to something
-        endpoints understands in create_request() and then translating the response
-        from endpoints back into something the backend understands in handle_request()
-
-        :returns: mixed, an instance of the backend class
-        """
-        return self.backend_class(**kwargs)
-
-    def create_request(self, raw_request, **kwargs):
-        """convert the raw interface raw_request to a request that endpoints understands
-
-        :params raw_request: mixed, this is the request given by backend
-        :params **kwargs:
-        :returns: an http.Request instance that endpoints understands
-        """
-        raise NotImplementedError()
-
-    def handle_request(self):
-        """this should be able to get a raw_request, pass it to create_call(),
-        then use the Call instance to handle the request, and then send a response
-        back to the backend
-        """
-        raise NotImplementedError()
-```
-
-These are the methods your new interface will need to define in order to work with Endpoints.
-
-You can look at the current built-in supported interfaces in `endpoints.interface` to see how they implemented the above `Server` class.
-
-
-## Creating a new websocket interface
-
-You can create a new websocket interface using `endpoints.interface.BaseWebsocketServer` which has some other methods that you can hook into to customize functionality:
-
-* `create_websocket_request`
-* `create_websocket_response_body`
-* `connect_websocket_call`
-* `create_websocket_call`
-* `disconnect_websocket_call`
-
-These are all already implemented but you can override them in order to customize how your websocket interface works. You can take a look at their method signatures in the code if you need to override them.
-
-
-### Payload class
-
-The `endpoints.interface.Payload` class is used by the `BaseWebsocketServer` to translate requests and responses to and from something that can be passed around.
-
-You can customize this class by creating an object that has `dumps` and `loads` class methods and then setting it:
-
-```python
-import json
-
-from endpoints interface import BaseWebsocketServer
-
-
-class JSONPayload(object):
-    @classmethod
-    def loads(cls, raw):
-        return json.loads(raw)
-
-    @classmethod
-    def dumps(cls, kwargs):
-        return json.dumps(kwargs)
-
-
-class WebsocketServer(BaseWebsocketServer):
-    payload_class = JSONPayload
-```
diff --git a/docs/PASSING_PARAMS.md b/docs/PASSING_PARAMS.md
index a2ff299..c26954c 100644
--- a/docs/PASSING_PARAMS.md
+++ b/docs/PASSING_PARAMS.md
@@ -1,13 +1,13 @@
 # Parameters
 
-You probably want to send some values to your server so _endpoints_ provides a helper decorator to make this process easier and customizable.
+At some point, you'll probably want to send some values to your server so _endpoints_ provides a helper decorator to make this process easier and customizable.
 
 
 ## param decorator
 
-The **param** decorator tries to act very similar api to Python's built-in [argparse.add_argument method](https://docs.python.org/2/library/argparse.html#adding-arguments) but has a more limited vocabulary of flags you can set, but should feel familiar.
+The **param** decorator tries to act very similar api to Python's built-in [argparse's add_argument method](https://docs.python.org/2/library/argparse.html#adding-arguments) but has a more limited vocabulary of flags you can set, but should feel familiar.
 
-By default the **param** decorator checks both _GET_ and _POST_ values with _POST_ taking precedence, if you would like to only check _POST_ parameters, use the **post_param** decorator, if you only want to check _GET_ parameters, then use the **get_param** decorator.
+By default the **param** decorator checks both _GET_ and _POST_ values with _POST_ taking precedence, if you would like to only check _POST_ parameters, use the **param_body** decorator, if you only want to check _GET_ parameters, then use the **param_query** decorator.
 
 The flags you can pass to the any of the decorators are:
 
@@ -22,7 +22,7 @@ The flags you can pass to the any of the decorators are:
     * append_list -- it's store_list + append, so `foo=1&foo=2,3` would be `foo=[1, 2, 3]`
 * default -- mixed -- the value that should be set if query param isn't there, if this is callable (eg, `time.time` or `datetime.utcnow`) then it will be called every time the decorated method is called
 * required -- boolean -- True if param is required, default is true
-* choices -- set() -- a set of values to be in tested against (eg, val in choices)
+* choices -- set -- a set of values to be in tested against (eg, val in choices)
 * allow_empty -- boolean -- True allows values like False, 0, '' through,
     default False, this will also let through any empty value that was set
     via the default flag
@@ -43,7 +43,7 @@ class Foo(Controller):
     @param('che', type=int, choices=[1, 2, 3], help="The specific che you want")
     @param('baz', required=False, help="You don't need to pass up a baz")
     @param('bam', default="bang", help="the bam you want, defaults to bang")
-    def POST(self, **kwargs):
+    async def POST(self, **kwargs):
         pass
 ```
 
@@ -54,9 +54,9 @@ from endpoints import Controller
 from endpoints.decorators import param
 
 class Foo(Controller):
-  @param('param1', default="some val")
-  @param('param2', choices=['one', 'two'])
-  def GET(self, **params): pass
+    @param('param1', default="some val")
+    @param('param2', choices=['one', 'two'])
+    async def GET(self, **params): pass
 ```
 
 
@@ -72,7 +72,7 @@ class Foo(Controller):
     @param(0, help="this will be in args[0]")
     @param(1, type=int, choices=[1, 2, 3], help="this will be in args[1]")
     @param('bar', help="this will be in kwargs['bar']")
-    def POST(self, *args, **kwargs):
+    async def POST(self, *args, **kwargs):
         pass
 ```
 
@@ -93,7 +93,7 @@ from endpoints.decorators import param
 
 class Foo(Controller):
     @param('bar', action="append_list", help="kwargs['bar'] will contain [1, 2, 3, 4, 5]")
-    def POST(self, **kwargs):
+    async def POST(self, **kwargs):
         pass
 ```
 
@@ -108,37 +108,37 @@ from endpoints.decorators import param
 
 class Foo(Controller):
     @param('bar', allow_empty=True, help="bar can now contain empty string")
-    def POST(self, **kwargs):
+    async def POST(self, **kwargs):
         pass
 ```
 
 
-## get_param decorator
+## param_query decorator
 
 Same as **param** but only checks **GET** query arguments passed in the actual url:
 
 ```python
 from endpoints import Controller
-from endpoints.decorators import get_param
+from endpoints.decorators import param_query
 
 class Foo(Controller):
-    @get_param('bar', help="the ?bar=... part of the url")
-    def GET(self, **kwargs):
+    @param_query('bar', help="the ?bar=... part of the url")
+    async def GET(self, **kwargs):
         pass
 ```
 
 
-## post_param decorator
+## param_body decorator
 
 Same as **param** but only checks **POST** body arguments posted to the given url.
 
 ```python
 from endpoints import Controller
-from endpoints.decorators import post_param
+from endpoints.decorators import param_body
 
 class Foo(Controller):
-    @post_param('bar', help="must be sent in the body of the request")
-    def POST(self, **kwargs):
+    @param_body('bar', help="must be sent in the body of the request")
+    async def POST(self, **kwargs):
         pass
 ```
 
diff --git a/docs/PREFIXES.md b/docs/PREFIXES.md
index 2e35ed4..ec1ea44 100644
--- a/docs/PREFIXES.md
+++ b/docs/PREFIXES.md
@@ -1,9 +1,10 @@
 # Controller Prefixes
 
-The controller prefix is a [module path](https://docs.python.org/3/tutorial/modules.html#the-module-search-path) that tells Endpoints where you have defined all your [Controllers](CONTROLLERS.md).
+The controller prefix is a [module search path](https://docs.python.org/3/tutorial/modules.html#the-module-search-path) that tells Endpoints where you have defined all your [Controllers](CONTROLLERS.md).
 
 Endpoints will only route to Controllers defined in these prefixes.
 
+
 ## Defining Controller prefixes
 
 Define controller prefixes is to set the environment variable `ENDPOINTS_PREFIX`, for example:
@@ -12,6 +13,7 @@ Define controller prefixes is to set the environment variable `ENDPOINTS_PREFIX`
 
 Now, when Endpoints is started it would route to any Controller class found in the `controllers` python module.
 
+
 ### Multiple Controller Prefixes
 
 You can define multiple controller prefixes through the environment also:
diff --git a/docs/RATELIMITING.md b/docs/RATELIMITING.md
index 16486ed..4522964 100644
--- a/docs/RATELIMITING.md
+++ b/docs/RATELIMITING.md
@@ -15,22 +15,20 @@ from endpoints.decorators.limit import ratelimit_ip
 
 
 class Default(Controller):
-    @limit_ip(limit=10, ttl=3600)
-    def GET(self):
+    @ratelimit_ip(limit=10, ttl=3600)
+    async def GET(self):
         return "hello world"
 ```
 
 That's it, now any unique ip request to `/` will be limited to 10 request every hour (3600 seconds)
 
 
-## The limit decorators
+## The Limit Decorators
 
-* **ratelimit_ip** - limit requests for a unique ip address.
-* **ratelimit** - limit requests for a unique ip address but you have to pass in `limit` and `ttl`.
-* **ratelimit_token** - limit requests for a unique _access_token_.
-* **ratelimit_param** - limit requests to a certain parameter.
-* **ratelimit_param_ip** - limit requests to a certain parameter and a unique ip address.
-* **ratelimit_param_only** - limit requests to a certain parameter with no restriction on uri path.
+* `ratelimit_ip` - limit requests for a unique ip address.
+* `ratelimit_access_token` - limit requests for a unique _access_token_.
+* `ratelimit_param` - limit requests to a certain parameter.
+* `ratelimit_param_ip` - limit requests to a certain parameter and a unique ip address.
 
 
 ## Customization
@@ -39,33 +37,35 @@ You can extend any of the limit decorators to fit them into your own system or c
 
 ```python
 from endpoints import Controller
-from endpoints.decorators.limit import RateLimitDecorator
+from endpoints.decorators.limit import RateLimitDecorator, RateLimitBackend
 
-class Backend(class):
+class Backend(RateLimitBackend):
     DEFAULT_LIMIT = 10
     DEFAULT_TTL = 3600
-    def target(self, request, key, limit, ttl):
+    async def handle(self, request, key, limit, ttl):
         if not limit:
             limit = self.DEFAULT_LIMIT
         if not ttl:
             ttl = self.DEFAULT_TTL
 
         # check key to see if it should be rejected based on limit and ttl
+        # and return True (the request is valid) or False (the request will
+        # be denied)
 
 class limit_user(RateLimitDecorator):
     backend_class = Backend
 
-    def normalize_key(self, request, *args, **kwargs):
-        user = some_call_that_returns_user_from_some_db_using_request_info(request)
+    async def normalize_key(self, request, *args, **kwargs):
+        user = await some_call_that_returns_user_from_some_db_using_request_info(request)
         return user.id if user else ""
 
 class Default(Controller):
     @limit_user
-    def GET(self):
+    async def GET(self):
         return "GET uses backend's default limit and ttl"
 
     @limit_user(5, 7200)
-    def POST(self):
+    async def POST(self):
         return "POST users limit and ttl passed into backend"
 ```
 
diff --git a/docs/README.md b/docs/README.md
index 0d4ad21..7b26046 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,6 +1,6 @@
 # Endpoints Documentation
 
-All the files in this directory should help you understant how to get the most from Endpoints.
+All the files in this directory should help you understand how to get the most from Endpoints.
 
 Just getting started? Read about [controllers](https://github.com/firstopinion/endpoints/blob/master/docs/CONTROLLERS.md) as that's the most important piece to understand, once controllers make sense, you can add some [decorators](https://github.com/firstopinion/endpoints/blob/master/docs/DECORATORS.md) to get the most out of each request.
 
diff --git a/docs/ROUTING.md b/docs/ROUTING.md
index 9a319f5..1dfb1e1 100644
--- a/docs/ROUTING.md
+++ b/docs/ROUTING.md
@@ -5,7 +5,7 @@ For the most part, routing is automatic depending on what you name your controll
 [README about controllers](https://github.com/firstopinion/endpoints/blob/master/docs/CONTROLLERS.md)
 
 
-## route decorator
+## The Route Decorator
 
 You can have different controller methods (eg, **GET**, **POST**) handle a given request depending on the request by using the `@route` decorator on your various Controller methods. So, let's say you have a `controllers.py` which contained:
 
@@ -17,17 +17,17 @@ from endpoints.decorators import route
 class Default(Controller):
     # this GET will handle / requests
     @route(lambda req: len(req.path_args) == 0)
-    def GET_1(self, username):
+    async def GET_1(self, username):
         return "/"
 
     # this GET will handle /:uid/:title requests
     @route(lambda req: len(req.path_args) == 2)
-    def GET_2(self, uid, title):
+    async def GET_2(self, uid, title):
         return "/:uid/:title"
 
     # this GET will handle /:username requests
     @route(lambda req: len(req.path_args) == 1)
-    def GET_3(self, username):
+    async def GET_3(self, username):
         return "/:username"
 ```
 
diff --git a/docs/TESTING.md b/docs/TESTING.md
deleted file mode 100644
index a78b7cf..0000000
--- a/docs/TESTING.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Testing
-
-I've been using a Vagrant box and pyenv. All testing takes place in the `/vagrant` folder on the Vagrant box, so after you ssh into the vagrant box:
-
-```
-$ cd /vagrant
-```
-
-
-## Running tests in Python 3
-
-```
-$ pyenv versions
-$ pyenv shell <INSTALLED_PYTHON_3_VERSION>
-$ python --version
-$ pyt3 -d
-```
-
-
-## Running tests in Python 2
-
-```
-$ pyenv versions
-$ pyenv shell <INSTALLED_PYTHON_2_VERSION>
-$ python --version
-$ pyt2 -d
-```
\ No newline at end of file
diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md
index 4bd884c..a57d728 100644
--- a/docs/VERSIONING.md
+++ b/docs/VERSIONING.md
@@ -16,20 +16,20 @@ from endpoints.decorators import version
 
 class Default(Controller):
   @version("", "v1") # handle any requests with no version or with v1
-  def GET_1(self):
+  async def GET_1(self):
     return "called version 1 /"
 
   @version("v2") # handle any requests with version v2
-  def GET_2(self):
+  async def GET_2(self):
     return "called version 2 /"
 
 class Foo(Controller):
   @version("v1")
-  def GET_1(self):
+  async def GET_1(self):
     return "called version 1 /foo"
 
   @version("v2")
-  def GET_2(self):
+  async def GET_2(self):
     return "called version 2 /foo"
 ```
 
diff --git a/endpoints/__init__.py b/endpoints/__init__.py
index f135d7b..f64d5d6 100644
--- a/endpoints/__init__.py
+++ b/endpoints/__init__.py
@@ -1,30 +1,19 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import logging
 
-
-# configure root endpoints logging handler to avoid "No handler found" warnings.
-# this has to go before importing child modules to make sure they don't configure
-# their loggers before Null logger is added
-logger = logging.getLogger(__name__)
-if not logger.handlers:
-    logger.addHandler(logging.NullHandler())
-
-
-from .reflection import Reflect, ReflectController, ReflectMethod
 from .exception import (
     CallError,
     Redirect,
     CallStop,
     AccessDenied,
+    RouteError,
+    VersionError,
     CloseConnection,
 )
-from .http import Request, Response, Url
-from .utils import AcceptHeader
-from .call import Controller, Router, Call
+from .utils import AcceptHeader, Url
+from .call import Controller, Request, Response
 from . import decorators
 from .decorators import param, route, version # this is for fluidity/convenience
 
 
-__version__ = '5.0.1'
+__version__ = '6.0.0'
 
diff --git a/endpoints/__main__.py b/endpoints/__main__.py
index 0a13099..7093cd1 100644
--- a/endpoints/__main__.py
+++ b/endpoints/__main__.py
@@ -1,25 +1,22 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import sys
 import os
 import argparse
 import logging
-import runpy
-import uuid
+
+from datatypes import ReflectName
 
 from endpoints import __version__
-#from endpoints.interface.wsgi import Server
-from endpoints import environ
-from endpoints.reflection import ReflectModule
+from endpoints.config import environ
 
 
 class Console(object):
     """Provides the CLI (command line interface) for running endpoints
 
     this is a class so subprojects can extend and manipulate how this works but
-    take advantage of the foundation that lays and the subprojects can have basically
-    similar interfaces to running on the command line
+    take advantage of the foundation that lays and the subprojects can have
+    basically similar interfaces to running on the command line
     """
     def __init__(self):
         self.environ = environ
@@ -27,75 +24,68 @@ def __init__(self):
 
     def __call__(self):
         """cli hook
+
         :return: integer, the exit code
         """
         ret_code = 0
 
-        args = self.parse_args()
+        args = self.parser.parse_args()
 
-        # we want to make sure the directory can be imported from since chances are
-        # the prefix module lives in that directory
+        # we want to make sure the directory can be imported from since chances
+        # are the prefix module lives in that directory
         sys.path.append(args.directory)
 
         if not args.quiet:
             # https://docs.python.org/2.7/library/logging.html#logging.basicConfig
-            logging.basicConfig(format="%(message)s", level=logging.DEBUG, stream=sys.stderr)
-            #logging.basicConfig(format='[%(levelname).1s|%(asctime)s|%(filename)s:%(lineno)s] %(message)s', level=logging.DEBUG, stream=sys.stderr)
+            logging.basicConfig(
+                format="%(message)s",
+                level=logging.DEBUG,
+                stream=sys.stderr
+            )
 
         logger = self.get_logger()
 
         self.environ.set_host(args.host)
         self.environ.set_controller_prefixes(args.prefix)
 
-        config = {}
         if args.file:
-            # load the configuration file
-            config = runpy.run_path(args.file)
-
-
-    #     if args.config_script:
-    #         # load a config script so you can customize the environment
-    #         h = "wsgiserver_config_{}".format(uuid.uuid4())
-    #         config_module = imp.load_source(h, args.config_script)
+            s = args.server_class(wsgifile=args.file)
 
-        s = args.server()
-        self.environ.set_host(s.hostloc)
+        else:
+            s = args.server_class()
 
-        if "application" in config:
-            s.application = config["application"]
+        # we reset the host and update the environment because the server
+        # could've set the port
+        hostloc = ":".join(map(str, s.server_address))
+        self.environ.set_host(hostloc)
 
         try:
             if args.count:
-                logger.info("Listening on {} for {} requests".format(s.hostloc, args.count))
+                logger.info("Listening on {} for {} requests".format(
+                    hostloc,
+                    args.count
+                ))
                 s.serve_count(args.count)
 
             else:
-                logger.info("Listening on {}".format(s.hostloc))
+                logger.info("Listening on {}".format(hostloc))
                 s.serve_forever()
 
         except KeyboardInterrupt:
             pass
 
+        finally:
+            logger.info("Server is shutting down")
+            s.server_close()
+
         return ret_code
 
     def get_logger(self):
         return logging.getLogger(__name__)
 
-    def get_version(self):
-        return __version__
-
-    def get_default_controller_prefixes(self):
-        return self.environ.get_controller_prefixes()
-
-    def parse_args(self):
-        return self.parser.parse_args()
-
-    def get_default_directory(self):
-        return os.getcwd()
-
     def create_parser(self):
         parser = argparse.ArgumentParser(
-            description='Start an endpoints WSGI server',
+            description='Start an endpoints server',
             add_help=True,
             # https://stackoverflow.com/a/12151325/5006
             formatter_class=argparse.ArgumentDefaultsHelpFormatter
@@ -103,7 +93,7 @@ def create_parser(self):
         parser.add_argument(
             "-v", "--version",
             action='version',
-            version="%(prog)s {}".format(self.get_version())
+            version="%(prog)s {}".format(__version__)
         )
         parser.add_argument(
             "--quiet",
@@ -113,7 +103,7 @@ def create_parser(self):
         parser.add_argument(
             '--prefix', "--controller-prefix", "-P",
             nargs="+",
-            default=self.get_default_controller_prefixes(),
+            default=self.environ.get_controller_prefixes(),
             help='The controller prefix(es) (python modpaths where Controller subclasses are found)'
         )
         parser.add_argument(
@@ -137,39 +127,30 @@ def create_parser(self):
         parser.add_argument(
             '--dir', "-D", "--directory",
             dest="directory",
-            default=self.get_default_directory(),
+            default=os.getcwd(),
             help='directory to run the server in, usually contains the prefix module path',
         )
         parser.add_argument(
             '--server', '-s',
-            dest="server",
-            default=self.get_default_server(),
-            type=self.get_server,
+            dest="server_class",
+            default="endpoints.interface.wsgi:Server",
+            type=self.get_server_class,
             help='The server interface endpoints will use',
         )
 
         return parser
 
-    def get_default_server(self):
-        return "endpoints.interface.wsgi.Server"
-
-    def get_server(self, classpath):
+    def get_server_class(self, classpath):
         """Returns the interface Server class
 
-        :param modpath: the module path of the interface (eg, endpoints.interface.wsgi)
+        :param modpath: the module path of the interface (eg,
+            endpoints.interface.wsgi)
         :returns: Server class
         """
-        parts = classpath.rsplit(".", 1)
-        modpath = parts[0]
-        classname = parts[1] if len(parts) > 1 else "Server"
-
-        try:
-            rm = ReflectModule(modpath)
-            s = getattr(rm.module, classname)
-
-        except (ImportError, AttributeError):
-            rm = ReflectModule("endpoints.interface.{}".format(modpath))
-            s = getattr(rm.module, classname)
+        classpath = ReflectName(classpath)
+        s = classpath.get_class()
+        if not s:
+            raise ValueError(f"Could not resolve {classpath}")
 
         return s
 
diff --git a/endpoints/call.py b/endpoints/call.py
index cf006d2..49d0938 100644
--- a/endpoints/call.py
+++ b/endpoints/call.py
@@ -1,469 +1,52 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import time
 import datetime
-import importlib
 import logging
 import os
-import sys
-import fnmatch
-import types
-import traceback
 import inspect
-import pkgutil
+import json
+import re
+import io
 
-from .utils import AcceptHeader, String
-from .http import Response, Request
+from .compat import *
+from .utils import AcceptHeader
 from .exception import (
     CallError,
-    Redirect,
-    CallStop,
-    AccessDenied,
     RouteError,
     VersionError,
-    CloseConnection,
 )
-from .compat import *
-from .reflection import ReflectModule, ReflectController, ReflectHTTPMethod
-from . import environ
-
-
-logger = logging.getLogger(__name__)
-
-
-class Call(object):
-    """The middleman
-
-    This class is created in the interface and is responsible for taking the request
-    and handling it and setting everything into the body of response so the interface
-    can respond to the request"""
-
-    # TODO -- 6-19-2017, I'm not wild about this property or what it does so I shouldn't
-    # rely on it existing in the future
-    quiet = False
-    """Set to True if you would like to avoid logging the request lifecycle"""
-
-    def __init__(self, req, res, rou):
-        self.request = req
-        self.response = res
-        self.router = rou
-        self.controller = None
-
-    def create_controller(self):
-        """Create a controller to handle the request
-
-        :returns: Controller, this Controller instance should be able to handle
-            the request
-        """
-        body = None
-        req = self.request
-        res = self.response
-        rou = self.router
-        con = None
-
-        controller_info = {}
-        try:
-            controller_info = rou.find(req, res)
-
-        except IOError as e:
-            logger.warning(str(e), exc_info=True)
-            raise CallError(
-                408,
-                "The client went away before the request body was retrieved."
-            )
-
-        except (ImportError, AttributeError, TypeError) as e:
-            exc_info = sys.exc_info()
-            logger.warning(str(e), exc_info=exc_info)
-            raise CallError(
-                404,
-                "{} not found because of {} \"{}\" on {}:{}".format(
-                    req.path,
-                    exc_info[0].__name__,
-                    str(e),
-                    os.path.basename(exc_info[2].tb_frame.f_code.co_filename),
-                    exc_info[2].tb_lineno
-                )
-            )
-
-        else:
-            con = controller_info['class_instance']
-
-        return con
-
-    def handle(self):
-        """Called from the interface to actually handle the request."""
-        body = None
-        req = self.request
-        res = self.response
-        rou = self.router
-        con = None
-        start = time.time()
-
-        try:
-            con = self.create_controller()
-            con.call = self
-            self.controller = con
-            if not self.quiet:
-                con.log_start(start)
-
-            # the controller handle method will manipulate self.response, it first
-            # tries to find a handle_HTTP_METHOD method, if it can't find that it
-            # will default to the handle method (which is implemented on Controller).
-            # method arguments are passed in so child classes can add decorators
-            # just like the HTTP_METHOD that will actually handle the request
-            controller_args, controller_kwargs = con.find_method_params()
-            controller_method = getattr(con, "handle_{}".format(req.method), None)
-            if not controller_method:
-                controller_method = getattr(con, "handle")
-
-            if not self.quiet:
-                logger.debug("Request handle method: {}.{}.{}".format(
-                    con.__class__.__module__,
-                    con.__class__.__name__,
-                    controller_method.__name__
-                ))
-            controller_method(*controller_args, **controller_kwargs)
-
-        except CloseConnection:
-            raise
-
-        except Exception as e:
-            self.handle_error(e) # this will manipulate self.response
-
-        finally:
-            if res.code == 204:
-                res.headers.pop('Content-Type', None)
-                res.body = None # just to be sure since body could've been ""
-
-            if con:
-                if not self.quiet:
-                    con.log_stop(start)
-
-        return res
-
-    def handle_error(self, e, **kwargs):
-        """if an exception is raised while trying to handle the request it will
-        go through this method
-
-        This method will set the response body and then also call Controller.handle_error
-        for further customization if the Controller is available
-
-        :param e: Exception, the error that was raised
-        :param **kwargs: dict, any other information that might be handy
-        """
-        req = self.request
-        res = self.response
-        con = self.controller
-
-        res.body = e
-
-        if isinstance(e, CallStop):
-            logger.debug(String(e), exc_info=True)
-            res.code = e.code
-            res.add_headers(e.headers)
-            res.body = e.body
-
-        elif isinstance(e, Redirect):
-            logger.debug(String(e), exc_info=True)
-            res.code = e.code
-            res.add_headers(e.headers)
-            res.body = None
-
-        elif isinstance(e, (AccessDenied, CallError)):
-            logger.warning(String(e), exc_info=True)
-            res.code = e.code
-            res.add_headers(e.headers)
-
-        elif isinstance(e, NotImplementedError):
-            logger.warning(String(e), exc_info=True)
-            res.code = 501
-
-        elif isinstance(e, TypeError):
-            e_msg = String(e)
-            controller_info = req.controller_info
-
-            # filter out TypeErrors raised from non handler methods
-            correct_prefix = e_msg.startswith(controller_info["method_prefix"]) or \
-                e_msg.startswith(controller_info["method_fallback"])
-
-            if correct_prefix and 'argument' in e_msg:
-                # there are subtle messaging differences between py2 and py3
-                pos_errs = ["takes exactly", "takes no arguments", "positional argument"]
-                if (pos_errs[0] in e_msg) or (pos_errs[1] in e_msg) or (pos_errs[2] in e_msg):
-                    # TypeError: <METHOD>() takes exactly M argument (N given)
-                    # TypeError: <METHOD>() takes no arguments (N given)
-                    # TypeError: <METHOD>() takes M positional arguments but N were given
-                    # we shouldn't ever get the "takes no arguments" case because of self,
-                    # but just in case
-                    # check if there are path args, if there are then 404, if not then 405
-                    logger.debug(e_msg, exc_info=True)
-
-                    if len(controller_info["method_args"]):
-                        res.code = 404
-
-                    else:
-                        res.code = 405
-
-                elif "unexpected keyword argument" in e_msg:
-                    # TypeError: <METHOD>() got an unexpected keyword argument '<NAME>'
-
-                    try:
-                        # if the binding of just the *args works then the
-                        # problem is the **kwargs so a 405 is appropriate,
-                        # otherwise return a 404
-                        inspect.getcallargs(controller_info["method"], *controller_info["method_args"])
-                        res.code = 405
-
-                        logger.warning("Controller method {}.{}.{}".format(
-                            controller_info['module_name'],
-                            controller_info['class_name'],
-                            e_msg
-                        ), exc_info=True)
-
-                    except TypeError:
-                        res.code = 404
-
-                elif "multiple values" in e_msg:
-                    # TypeError: <METHOD>() got multiple values for keyword argument '<NAME>'
-                    try:
-                        inspect.getcallargs(controller_info["method"], *controller_info["method_args"])
-                        res.code = 409
-                        logger.warning(e)
-
-                    except TypeError:
-                        res.code = 404
-
-                else:
-                    res.code = 500
-                    logger.exception(e)
-
-            else:
-                res.code = 500
-                logger.exception(e)
-
-        else:
-            res.code = 500
-            logger.exception(e)
-
-        if con:
-            error_method = getattr(con, "handle_{}_error".format(res.code), None)
-            if not error_method:
-                error_method = getattr(con, "handle_{}_error".format(req.method), None)
-                if not error_method:
-                    error_method = getattr(con, "handle_error")
-
-            logger.debug("Handle {} error using method: {}.{}".format(
-                res.code,
-                con.__class__.__name__,
-                error_method.__name__
-            ))
-            error_method(e, **kwargs)
-
-
-class Router(object):
-    """
-    Where all the routing magic happens, this takes an incoming URI and gathers
-    the information needed to turn that URI into a Controller
-
-    we always translate an HTTP request using this pattern: METHOD /module/class/args?kwargs
-
-    GET /foo -> controller_prefix.foo.Default.GET
-    POST /foo/bar -> controller_prefix.foo.Bar.POST
-    GET /foo/bar/che -> controller_prefix.foo.Bar.GET(che)
-    POST /foo/bar/che?baz=foo -> controller_prefix.foo.Bar.POST(che, baz=foo)
-    """
-    default_class_name = "Default"
-
-    _module_name_cache = {}
-
-    @property
-    def module_names(self):
-        """get all the modules in the controller_prefixes
-
-        :returns: set, a set of string module names
-        """
-        ret = set()
-        _module_name_cache = type(self)._module_name_cache
+from .config import environ
 
-        for controller_prefix in self.controller_prefixes:
-
-            if controller_prefix in _module_name_cache:
-                ret.update(_module_name_cache[controller_prefix])
-
-            else:
-                logger.debug("Populating module cache for controller_prefix {}".format(controller_prefix))
-                rm = ReflectModule(controller_prefix)
-                module_names = rm.module_names
-
-                #_module_name_cache.setdefault(controller_prefix, {})
-                type(self)._module_name_cache[controller_prefix] = module_names
-                ret.update(module_names)
-
-        return ret
-
-    def __init__(self, controller_prefixes):
-        if not controller_prefixes:
-            raise ValueError("controller_prefixes is empty")
-
-        self.controller_prefixes = controller_prefixes
-
-    def find(self, req, res):
-        ret = {}
-        controller_path = []
-
-        logger.debug("Searching for Controller using path: {}".format(req.path))
-
-        controller_prefix, module_name, module_path, controller_method_args = self.get_module_name(
-            list(req.path_args)
-        )
-        controller_module_name = module_name
-        controller_module_r = ReflectModule(module_name)
-        controller_module = controller_module_r.module
-
-        controller_class = None
-        if controller_method_args:
-            controller_class = self.get_class(
-                controller_module,
-                controller_method_args[0]
-            )
-
-        if controller_class:
-            controller_path.append(controller_method_args.pop(0))
-            controller_class_name = controller_class.__name__
-
-        else:
-            controller_class_name = self.default_class_name
-            controller_class = self.get_class(controller_module, controller_class_name)
-
-        if not controller_class:
-            raise TypeError(
-                "Could not find a valid module and Controller class for {}".format(
-                    req.path
-                )
-            )
-
-        ret['controller_prefix'] = controller_prefix
-        ret['module'] = controller_module
-        ret['module_reflection'] = controller_module_r
-        ret['module_name'] = controller_module_name
-        ret['module_path'] = "/".join(module_path)
-
-        ret['class'] = controller_class
-        ret['class_reflection'] = ReflectController(
-            controller_module_r,
-            controller_class,
-            controller_prefix
-        )
-        ret['class_name'] = controller_class_name
-        ret['class_instance'] = self.get_class_instance(req, res, controller_class)
-        ret['class_path'] = "/".join(controller_path)
-
-        # we merge the leftover path args with the body kwargs
-        controller_method_args.extend(req.body_args)
-
-        ret['method_args'] = controller_method_args
-        ret['method_kwargs'] = req.kwargs
-
-        ret["method_prefix"] = req.method.upper()
-        ret["method_fallback"] = "ANY"
-
-        req.controller_info = ret
-        return ret
-
-    def get_class_instance(self, req, res, controller_class):
-        instance = controller_class(req, res)
-        instance.router = self
-        return instance
-
-    def get_module_name(self, path_args):
-        """returns the module_name and remaining path args.
-
-        :returns: tuple, (controller_prefix, module_name, module_path, path_args),
-            where controller_prefix is the prefix the module was found in and module_name
-            is the python module path (eg, foo.bar.che) and module_path is a list
-            of the different parts (eg, ["foo", "bar", "che"]) and path_args are
-            the remaining path_args after finding the module
-        """
-        module_name = ""
-        module_path = []
-
-        # using the path_args we are going to try and find the best module path
-        # for the request
-        if path_args:
-            cset = self.module_names
-            for controller_prefix in self.controller_prefixes:
-                mod_name = controller_prefix + "." + path_args[0]
-                if mod_name in cset:
-                    module_name = mod_name
-                    module_path.append(path_args.pop(0))
-
-                    while path_args:
-                        mod_name += "." + path_args[0]
-                        if mod_name in cset:
-                            module_name = mod_name
-                            module_path.append(path_args.pop(0))
-                        else:
-                            break
-
-                    break
-
-        if not module_name:
-            # we didn't find the correct module using module paths, so now let's
-            # try class paths, first found class path wins
-            default_module_name = ""
-
-            for controller_prefix in self.controller_prefixes:
-                controller_module = ReflectModule(controller_prefix).module
-                if path_args:
-                    controller_class = self.get_class(controller_module, path_args[0])
-                    if controller_class:
-                        module_name = controller_prefix
-                        break
-
-                if not default_module_name:
-                    # look for the default class just in case
-                    controller_class = self.get_class(controller_module, self.default_class_name)
-                    if controller_class:
-                        default_module_name = controller_prefix
-
-            if not module_name:
-                if default_module_name:
-                    module_name = default_module_name
-                    controller_prefix = module_name
-
-                else:
-                    raise TypeError(
-                        "Could not find a valid module with path {} and controller_prefixes {}".format(
-                            "/".join(path_args),
-                            self.controller_prefixes
-                        )
-                    )
-                    #module_name = self.controller_prefixes[0]
+from datatypes import (
+    HTTPHeaders as Headers,
+    property as cachedproperty,
+)
 
-        return controller_prefix, module_name, module_path, path_args
+from .compat import *
+from .utils import (
+    AcceptHeader,
+    MimeType,
+    Base64,
+    Deepcopy,
+    Url,
+    Status,
+)
 
-    def get_class(self, module, class_name):
-        """try and get the class_name from the module and make sure it is a valid
-        controller"""
-        # let's get the class
-        class_name = class_name.capitalize()
-        class_object = getattr(module, class_name, None)
-        if not class_object or not issubclass(class_object, Controller):
-            class_object = None
 
-        return class_object
+logger = logging.getLogger(__name__)
 
 
 class Controller(object):
     """
     this is the interface for a Controller sub class
 
-    All your controllers MUST extend this base class, since it ensures a proper interface :)
+    All your controllers MUST extend this base class, since it ensures a proper
+    interface :)
 
-    to activate a new endpoint, just add a module on your PYTHONPATH.controller_prefix that has a class
-    that extends this class, and then defines at least one http method (like GET or POST), so if you
-    wanted to create the endpoint /foo/bar (with controller_prefix che), you would just need to:
+    to activate a new endpoint, just add a module on your PYTHONPATH
+    controller_prefix that has a class that extends this class, and then defines
+    at least one http method (like GET or POST), so if you wanted to create the
+    endpoint /foo/bar (with controller_prefix che), you would just need to:
 
     :Example:
         # che/foo.py
@@ -473,22 +56,23 @@ class Bar(endpoints.Controller):
             def GET(self, *args, **kwargs):
                 return "you just made a GET request to /foo/bar"
 
-    as you support more methods, like POST and PUT, you can just add POST() and PUT()
-    methods to your Bar class and Bar will support those http methods. Although you can
-    request any method (a method is valid if it is all uppercase), here is a list of
-    rfc approved http request methods:
+    as you support more methods, like POST and PUT, you can just add POST() and
+    PUT() methods to your Bar class and Bar will support those http methods.
+    Although you can request any method (a method is valid if it is all
+    uppercase), here is a list of rfc approved http request methods:
 
-    http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
+        http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
 
-    If you would like to create a base controller that other controllers will extend and don't
-    want that controller to be picked up by reflection, just start the classname with an underscore:
+    If you would like to create a base controller that other controllers will
+    extend and don't want that controller to be picked up by reflection, just
+    start the classname with an underscore:
 
     :Example:
         import endpoints
 
         class _BaseController(endpoints.Controller):
             def GET(self, *args, **kwargs):
-                return "every controller that extends this will have this GET method"
+                return "every child that extends this will have this GET method"
     """
     request = None
     """holds a Request() instance"""
@@ -496,44 +80,38 @@ def GET(self, *args, **kwargs):
     response = None
     """holds a Response() instance"""
 
-    call = None
-    """holds the call() instance that created this Controller"""
-
-    router = None
-    """holds the Router() instance that found this controller"""
-
     private = False
-    """set this to True if the controller should not be picked up by reflection, the controller
-    will still be available, but reflection will not reveal it as an endpoint"""
+    """set this to True if the controller should not be picked up by reflection,
+    the controller will still be available, but reflection will not reveal it as
+    an endpoint"""
 
     cors = True
     """Activates CORS support, http://www.w3.org/TR/cors/"""
 
-    @property
+    @cachedproperty(cached="_encoding")
     def encoding(self):
         """the response charset of this controller"""
         req = self.request
         encoding = req.accept_encoding
         return encoding if encoding else environ.ENCODING
 
-    @property
+    @cachedproperty(cached="_content_type")
     def content_type(self):
         """the response content type this controller will use"""
         req = self.request
         content_type = req.accept_content_type
         return content_type if content_type else environ.RESPONSE_CONTENT_TYPE
 
-    def __init__(self, request, response, *args, **kwargs):
+    def __init__(self, request, response, **kwargs):
         self.request = request
         self.response = response
         self.logger = self.create_logger(request, response)
-        super(Controller, self).__init__(*args, **kwargs)
 
-    def OPTIONS(self, *args, **kwargs):
+    async def OPTIONS(self, *args, **kwargs):
         """Handles CORS requests for this controller
 
-        if self.cors is False then this will raise a 405, otherwise it sets everything
-        necessary to satisfy the request in self.response
+        if self.cors is False then this will raise a 405, otherwise it sets
+        everything necessary to satisfy the request in self.response
         """
         if not self.cors:
             raise CallError(405)
@@ -543,6 +121,7 @@ def OPTIONS(self, *args, **kwargs):
         origin = req.get_header('origin')
         if not origin:
             raise CallError(400, 'Need Origin header') 
+
         call_headers = [
             ('Access-Control-Request-Headers', 'Access-Control-Allow-Headers'),
             ('Access-Control-Request-Method', 'Access-Control-Allow-Methods')
@@ -560,20 +139,10 @@ def OPTIONS(self, *args, **kwargs):
         }
         self.response.add_headers(other_headers)
 
-    def create_logger(self, request, response):
-        # we use self.logger and set the name to endpoints.call.module.class so
-        # you can filter all controllers using endpoints.call, filter all
-        # controllers in a certain module using endpoints.call.module or just a
-        # specific controller using endpoints.call.module.class
-        logger_name = logger.name
-        class_name = self.__class__.__name__
-        module_name = self.__class__.__module__
-        return logging.getLogger("{}.{}.{}".format(logger_name, class_name, module_name))
-
-    def prepare_response(self):
-        """Called at the beginning of the handle() call, use to prepare the response
-        instance with defaults that can be overridden in the controller's actual
-        http handle method"""
+    async def prepare_response(self):
+        """Called at the beginning of the handle() call, use to prepare the
+        response instance with defaults that can be overridden in the
+        controller's actual http handle method"""
         req = self.request
         res = self.response
 
@@ -586,7 +155,7 @@ def prepare_response(self):
             encoding
         ))
 
-    def handle_origin(self, origin):
+    async def handle_origin(self, origin):
         """Check the origin and decide if it is valid
 
         :param origin: string, this can be empty or None, so you'll need to handle
@@ -595,11 +164,12 @@ def handle_origin(self, origin):
         """
         return True
 
-    def handle_cors(self):
-        """This will set the headers that are needed for any cors request (OPTIONS or real) """
+    async def handle_cors(self):
+        """This will set the headers that are needed for any cors request
+        (OPTIONS or real) """
         req = self.request
         origin = req.get_header('origin')
-        if self.handle_origin(origin):
+        if await self.handle_origin(origin):
             if origin:
                 # your server must read the value of the request's Origin header
                 # and use that value to set Access-Control-Allow-Origin, and must
@@ -617,7 +187,7 @@ def handle_cors(self):
             # https://stackoverflow.com/q/28553580/5006
             raise CallError(403)
 
-    def handle(self, *controller_args, **controller_kwargs):
+    async def handle(self, *controller_args, **controller_kwargs):
         """handles the request and returns the response
 
         This should set any response information directly onto self.response
@@ -630,18 +200,18 @@ def handle(self, *controller_args, **controller_kwargs):
         :param **controller_kwargs: dict, the query and body params merged together
         """
         if self.cors:
-            self.handle_cors()
+            await self.handle_cors()
 
-        self.prepare_response()
+        await self.prepare_response()
 
         req = self.request
         res = self.response
 
         # the @route* and @version decorators have a catastrophic error handler
-        # that will be called if all if all found methods failed to resolve
+        # that will be called if all found methods failed to resolve
         res_error_handler = None
 
-        controller_methods = self.find_methods()
+        controller_methods = await self.find_methods()
         for controller_method_name, controller_method in controller_methods:
             req.controller_info["method_name"] = controller_method_name
             req.controller_info["method"] = controller_method
@@ -654,40 +224,67 @@ def handle(self, *controller_args, **controller_kwargs):
                     req.controller_info['class_name'],
                     controller_method_name
                 ))
-                res.body = controller_method(
-                    *controller_args,
-                    **controller_kwargs
-                )
+
+                if inspect.iscoroutinefunction(controller_method):
+                    res.body = await controller_method(
+                        *controller_args,
+                        **controller_kwargs
+                    )
+
+                else:
+                    res.body = controller_method(
+                        *controller_args,
+                        **controller_kwargs
+                    )
 
                 res_error_handler = None
                 break
 
             except VersionError as e:
                 if not res_error_handler:
-                    res_error_handler = getattr(e.instance, "handle_failure", None)
+                    res_error_handler = getattr(
+                        e.instance,
+                        "handle_failure",
+                        None
+                    )
 
-                self.logger.debug("Request Controller method: {}.{}.{} failed version check [{} not in {}]".format(
-                    req.controller_info['module_name'],
-                    req.controller_info['class_name'],
-                    controller_method_name,
-                    e.request_version,
-                    e.versions
-                ))
+                self.logger.debug(
+                    " ".join([
+                        "Request Controller method: {}.{}.{}".format(
+                            req.controller_info['module_name'],
+                            req.controller_info['class_name'],
+                            controller_method_name,
+                        ),
+                        "failed version check [{} not in {}]".format(
+                            e.request_version,
+                            e.versions,
+                        ),
+                    ])
+                )
 
             except RouteError as e:
                 if not res_error_handler:
-                    res_error_handler = getattr(e.instance, "handle_failure", None)
+                    res_error_handler = getattr(
+                        e.instance,
+                        "handle_failure",
+                        None
+                    )
 
-                self.logger.debug("Request Controller method: {}.{}.{} failed routing check".format(
-                    req.controller_info['module_name'],
-                    req.controller_info['class_name'],
-                    controller_method_name
-                ))
+                self.logger.debug(
+                    " ".join([
+                        "Request Controller method: {}.{}.{}".format(
+                            req.controller_info['module_name'],
+                            req.controller_info['class_name'],
+                            controller_method_name,
+                        ),
+                        "failed routing check",
+                    ])
+                )
 
         if res_error_handler:
-            res_error_handler(self)
+            await res_error_handler(self)
 
-    def handle_error(self, e, **kwargs):
+    async def handle_error(self, e, **kwargs):
         """if an exception is raised while trying to handle the request it will
         go through this method, this method is called from the Call instance
 
@@ -696,7 +293,7 @@ def handle_error(self, e, **kwargs):
         """
         pass
 
-    def find_methods(self):
+    async def find_methods(self):
         """Find the methods that could satisfy this request
 
         This will go through and find any method that starts with the request.method,
@@ -731,14 +328,25 @@ def find_methods(self):
                     # if we have method args and we don't have a method to even
                     # answer the request it should be a 404 since the path is
                     # invalid
-                    raise CallError(404)
+                    raise CallError(
+                        404,
+                        "Could not find a {} method for path {}".format(
+                            method_name,
+                            req.path,
+                        )
+                    )
 
                 else:
                     # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
                     # and 501 (Not Implemented) if the method is unrecognized or not
                     # implemented by the origin server
-                    self.logger.warning("No methods to handle {} found".format(method_name), exc_info=True)
-                    raise CallError(501, "{} {} not implemented".format(req.method, req.path))
+                    self.logger.warning(
+                        "No methods to handle {} found".format(method_name)
+                    )
+                    raise CallError(
+                        501,
+                        "{} {} not implemented".format(req.method, req.path)
+                    )
 
         elif len(methods) > 1 and method_name in method_names:
             raise ValueError(
@@ -756,15 +364,19 @@ def find_methods(self):
 
         return methods
 
-    def find_method_params(self):
-        """Return the method params
-
-        :returns: tuple (args, kwargs) that will be passed as *args, **kwargs
-        """
-        req = self.request
-        args = req.controller_info["method_args"]
-        kwargs = req.controller_info["method_kwargs"]
-        return args, kwargs
+    def create_logger(self, request, response):
+        # we use self.logger and set the name to endpoints.call.module.class so
+        # you can filter all controllers using endpoints.call, filter all
+        # controllers in a certain module using endpoints.call.module or just a
+        # specific controller using endpoints.call.module.class
+        logger_name = logger.name
+        module_name = self.__class__.__module__
+        class_name = self.__class__.__name__
+        return logging.getLogger("{}.{}.{}".format(
+            logger_name,
+            module_name,
+            class_name,
+        ))
 
     def log_start(self, start):
         """log all the headers and stuff at the start of the request"""
@@ -777,13 +389,25 @@ def log_start(self, start):
                 uuid += " "
 
             if req.query:
-                self.logger.info("Request {}method: {} {}?{}".format(uuid, req.method, req.path, req.query))
+                self.logger.info("Request {}method: {} {}?{}".format(
+                    uuid,
+                    req.method,
+                    req.path,
+                    req.query
+                ))
+
             else:
-                self.logger.info("Request {}method: {} {}".format(uuid, req.method, req.path))
+                self.logger.info("Request {}method: {} {}".format(
+                    uuid,
+                    req.method,
+                    req.path
+                ))
 
             self.logger.info("Request {}date: {}".format(
                 uuid,
-                datetime.datetime.utcfromtimestamp(start).strftime("%Y-%m-%dT%H:%M:%S.%f"),
+                datetime.datetime.utcfromtimestamp(start).strftime(
+                    "%Y-%m-%dT%H:%M:%S.%f"
+                ),
             ))
 
             ip = req.ip
@@ -791,7 +415,10 @@ def log_start(self, start):
                 self.logger.info("Request {}IP address: {}".format(uuid, ip))
 
             if 'authorization' in req.headers:
-                self.logger.info('Request {}auth: {}'.format(uuid, req.headers['authorization']))
+                self.logger.info('Request {}auth: {}'.format(
+                    uuid,
+                    req.headers['authorization']
+                ))
 
             ignore_hs = set([
                 'accept-language',
@@ -840,21 +467,583 @@ def log_stop(self, start):
 
         res = self.response
         req = self.request
-        uuid = getattr(req, "uuid", "")
-        if uuid:
+        if uuid := getattr(req, "uuid", ""):
             uuid += " "
 
         for k, v in res.headers.items():
-            self.logger.info("Request {}response header {}: {}".format(uuid, k, v))
+            self.logger.info("Response {}header {}: {}".format(uuid, k, v))
 
         stop = time.time()
-        get_elapsed = lambda start, stop, multiplier, rnd: round(abs(stop - start) * float(multiplier), rnd)
+        def get_elapsed(start, stop, multiplier, rnd):
+            return round(abs(stop - start) * float(multiplier), rnd)
         elapsed = get_elapsed(start, stop, 1000.00, 1)
         total = "%0.1f ms" % (elapsed)
-        self.logger.info("Request {}response {} {} in {}".format(
+        self.logger.info("Response {}{} {} in {}".format(
             uuid,
             self.response.code,
             self.response.status,
             total
         ))
 
+
+class Call(object):
+    header_class = Headers
+
+    def __init__(self):
+        self.headers = Headers()
+
+    def has_header(self, header_name):
+        """return true if the header is set"""
+        return header_name in self.headers
+
+    def set_headers(self, headers):
+        """replace all headers with passed in headers"""
+        self.headers = Headers(headers)
+
+    def add_headers(self, headers, **kwargs):
+        self.headers.update(headers, **kwargs)
+
+    def set_header(self, header_name, val):
+        self.headers[header_name] = val
+
+    def add_header(self, header_name, val, **params):
+        self.headers.add_header(header_name, val, **params)
+
+    def get_header(self, header_name, default_val=None, allow_empty=True):
+        """try as hard as possible to get a a response header of header_name,
+        return default_val if it can't be found"""
+        v = self.headers.get(header_name, default_val)
+        if v:
+            return v
+
+        else:
+            if not allow_empty:
+                return default_val
+
+    def find_header(self, header_names, default_val=None, allow_empty=True):
+        """given a list of headers return the first one you can, default_val if
+        you don't find any
+
+        :param header_names: list, a list of headers, first one found is
+            returned
+        :param default_val: mixed, returned if no matching header is found
+        :returns: mixed, the value of the header or default_val
+        """
+        ret = default_val
+        for header_name in header_names:
+            if self.has_header(header_name):
+                ret = self.get_header(header_name, default_val)
+                if ret or allow_empty:
+                    break
+
+        if not ret and not allow_empty:
+            ret = default_val
+
+        return ret
+
+    def _parse_query_str(self, query):
+        """return name=val&name2=val2 strings into {name: val} dict"""
+        u = Url(query=query)
+        return u.query_kwargs
+
+    def _build_body_str(self, b):
+        # we are returning the body, let's try and be smart about it and match
+        # content type
+        ct = self.get_header('content-type')
+        if ct:
+            ct = ct.lower()
+            if ct.rfind("json") >= 0:
+                if b:
+                    b = json.dumps(b)
+                else:
+                    b = None
+
+            elif ct.rfind("x-www-form-urlencoded") >= 0:
+                b = urlencode(b, doseq=True)
+
+        return b
+
+    def copy(self):
+        """nice handy wrapper around the deepcopy"""
+        return self.__deepcopy__()
+
+    def __deepcopy__(self, memodict=None):
+        memodict = memodict or {}
+
+        memodict.setdefault("controller_info", None)
+        memodict.setdefault("raw_request", None)
+        memodict.setdefault("body", getattr(self, "body", None))
+
+        return Deepcopy(ignore_private=True).copy(self, memodict)
+
+    def is_json(self):
+        return self.headers.is_json()
+
+
+class Request(Call):
+    '''
+    common interface that endpoints uses to decide what to do with the incoming
+    request
+
+    an instance of this class is used by the endpoints Call instance to decide
+    where endpoints should route requests, so, many times, you'll need to write
+    a glue function that takes however your request data is passed to Python and
+    convert it into a Request instance that endpoints can understand
+
+    properties:
+        * headers -- a dict of all the request headers
+        * path -- the /path/part/of/the/url
+        * path_args -- tied to path, it's path, but divided by / so all the path
+            bits are returned as a list
+        * query -- the ?name=val portion of a url
+        * query_kwargs -- tied to query, the values in query but converted to a
+            dict {name: val}
+    '''
+    raw_request = None
+    """the original raw request that was filtered through one of the interfaces"""
+
+    method = None
+    """the http method (GET, POST)"""
+
+    controller_info = None
+    """will hold the controller information for the request, populated from the Call"""
+
+    @cachedproperty(cached="_uuid")
+    def uuid(self):
+        # if there is an X-uuid header then set uuid and send it down
+        # with every request using that header
+        # https://stackoverflow.com/questions/18265128/what-is-sec-websocket-key-for
+        uuid = None
+
+        # first try and get the uuid from the body since javascript has limited
+        # capability of setting headers for websockets
+        kwargs = self.kwargs
+        if "uuid" in kwargs:
+            uuid = kwargs["uuid"]
+
+        # next use X-UUID header, then the websocket key
+        if not uuid:
+            uuid = self.find_header(["X-UUID", "Sec-Websocket-Key"])
+
+        return uuid or ""
+
+    @cachedproperty(cached="_accept_content_type")
+    def accept_content_type(self):
+        """Return the requested content type
+
+        https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+
+        :returns: string, empty if a suitable content type wasn't found, this will
+            only check the first accept content type and then only if that content
+            type has no wildcards
+        """
+        v = ""
+        accept_header = self.get_header('accept', "")
+        if accept_header:
+            a = AcceptHeader(accept_header)
+            for mt in a:
+                # we only care about the first value, and only if it has no wildcards
+                if "*" not in mt[0]:
+                    v = "/".join(mt[0])
+                break
+
+        return v
+
+    @cachedproperty(cached="_accept_encoding")
+    def accept_encoding(self):
+        """The encoding the client requested the response to use"""
+        # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
+        ret = ""
+        accept_encoding = self.get_header("Accept-Charset", "")
+        if accept_encoding:
+            bits = re.split(r"\s+", accept_encoding)
+            bits = bits[0].split(";")
+            ret = bits[0]
+        return ret
+
+    @cachedproperty(cached="_encoding")
+    def encoding(self):
+        """the character encoding of the request, usually only set in POST type requests"""
+        encoding = None
+        ct = self.get_header('content-type')
+        if ct:
+            ah = AcceptHeader(ct)
+            if ah.media_types:
+                encoding = ah.media_types[0][2].get("charset", None)
+
+        return encoding
+
+    @property
+    def access_token(self):
+        """return an Oauth 2.0 Bearer access token if it can be found"""
+        access_token = self.get_auth_bearer()
+        if not access_token:
+            access_token = self.query_kwargs.get('access_token', '')
+            if not access_token:
+                access_token = self.body_kwargs.get('access_token', '')
+
+        return access_token
+
+    @property
+    def client_tokens(self):
+        """try and get Oauth 2.0 client id and secret first from basic auth header,
+        then from GET or POST parameters
+
+        return -- tuple -- client_id, client_secret
+        """
+        client_id, client_secret = self.get_auth_basic()
+
+        if not client_id:
+            client_id = self.query_kwargs.get('client_id', '')
+            if not client_id:
+                client_id = self.body_kwargs.get('client_id', '')
+
+        if not client_secret:
+            client_secret = self.query_kwargs.get('client_secret', '')
+            if not client_secret:
+                client_secret = self.body_kwargs.get('client_secret', '')
+
+        return client_id, client_secret
+
+    @cachedproperty(read_only="_ips")
+    def ips(self):
+        """return all the possible ips of this request, this will include public
+        and private ips"""
+        r = []
+        names = ['X_FORWARDED_FOR', 'CLIENT_IP', 'X_REAL_IP', 'X_FORWARDED', 
+               'X_CLUSTER_CLIENT_IP', 'FORWARDED_FOR', 'FORWARDED', 'VIA',
+               'REMOTE_ADDR']
+
+        for name in names:
+            vs = self.get_header(name, '')
+            if vs:
+                r.extend(map(lambda v: v.strip(), vs.split(',')))
+
+        return r
+
+    @cachedproperty(read_only="_ip")
+    def ip(self):
+        """return the public ip address"""
+        r = ''
+
+        # this was compiled from here:
+        # https://github.com/un33k/django-ipware
+        # http://www.ietf.org/rfc/rfc3330.txt (IPv4)
+        # http://www.ietf.org/rfc/rfc5156.txt (IPv6)
+        # https://en.wikipedia.org/wiki/Reserved_IP_addresses
+        format_regex = re.compile(r'\s')
+        ip_regex = re.compile(r'^(?:{})'.format(r'|'.join([
+            r'0\.', # reserved for 'self-identification'
+            r'10\.', # class A
+            r'169\.254', # link local block
+            r'172\.(?:1[6-9]|2[0-9]|3[0-1])\.', # class B
+            r'192\.0\.2\.', # documentation/examples
+            r'192\.168', # class C
+            r'255\.{3}', # broadcast address
+            r'2001\:db8', # documentation/examples
+            r'fc00\:', # private
+            r'fe80\:', # link local unicast
+            r'ff00\:', # multicast
+            r'127\.', # localhost
+            r'\:\:1' # localhost
+        ])))
+
+        ips = self.ips
+        for ip in ips:
+            if not format_regex.search(ip) and not ip_regex.match(ip):
+                r = ip
+                break
+
+        return r
+
+    @cachedproperty(cached="_host")
+    def host(self):
+        """return the request host"""
+        return self.get_header("host")
+
+    @cachedproperty(cached="_scheme")
+    def scheme(self):
+        """return the request scheme (eg, http, https)"""
+        return "http"
+
+    @cachedproperty(cached="_port")
+    def port(self):
+        """return the server port"""
+        _, port = Url.split_hostname_from_port(self.host)
+        return port
+
+    @property
+    def url(self):
+        """return the full request url as an Url() instance"""
+        scheme = self.scheme
+        host = self.host
+        path = self.path
+        query = self.query
+        port = self.port
+
+        # normalize the port
+        hostname, host_port = Url.split_hostname_from_port(host)
+        if host_port:
+            port = host_port
+
+        class_path = ""
+        module_path = ""
+        if self.controller_info:
+            class_path = self.controller_info.get("class_path", "")
+            module_path = self.controller_info.get("module_path", "")
+
+        u = Url(
+            scheme=scheme,
+            hostname=hostname,
+            path=path,
+            query=query,
+            port=port,
+            class_path=class_path,
+            module_path=module_path
+        )
+        return u
+
+    @cachedproperty(cached="_path")
+    def path(self):
+        """path part of a url (eg, http://host.com/path?query=string)"""
+        self._path = ''
+        path_args = self.path_args
+        path = "/{}".format("/".join(path_args))
+        return path
+
+    @cachedproperty(cached="_path_args")
+    def path_args(self):
+        """the path converted to list (eg /foo/bar becomes [foo, bar])"""
+        self._path_args = []
+        path = self.path
+        path_args = list(filter(None, path.split('/')))
+        return path_args
+
+    @cachedproperty(cached="_query")
+    def query(self):
+        """query_string part of a url (eg, http://host.com/path?query=string)"""
+        self._query = query = ""
+
+        query_kwargs = self.query_kwargs
+        if query_kwargs: query = urlencode(query_kwargs, doseq=True)
+        return query
+
+    @cachedproperty(cached="_query_kwargs")
+    def query_kwargs(self):
+        """{foo: bar, baz: che}"""
+        self._query_kwargs = query_kwargs = {}
+        query = self.query
+        if query: query_kwargs = self._parse_query_str(query)
+        return query_kwargs
+
+    @property
+    def kwargs(self):
+        """combine GET and POST params to be passed to the controller"""
+        kwargs = dict(self.query_kwargs)
+        kwargs.update(self.body_kwargs)
+        return kwargs
+
+    def __init__(self):
+        self.body = None
+        self.body_args = []
+        self.body_kwargs = {}
+        super().__init__()
+
+    def version(self, content_type="*/*"):
+        """by default, versioning is based off of this post 
+        http://urthen.github.io/2013/05/09/ways-to-version-your-api/
+        https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+
+        This can be extended to implement versioning in any other way though,
+        this is used with the @version decorator
+
+        :param content_type: string, the content type you want to check version
+            info, by default this checks all content types, which is probably
+            what most want, since if you're doing accept header versioning
+            you're probably only passing up one content type
+        :returns: string, the found version
+        """
+        v = ""
+        accept_header = self.get_header('accept', "")
+        if accept_header:
+            a = AcceptHeader(accept_header)
+            for mt in a.filter(content_type):
+                v = mt[2].get("version", "")
+                if v: break
+
+        return v
+
+    def is_method(self, method):
+        """return True if the request method matches the passed in method"""
+        return self.method.upper() == method.upper()
+
+    def has_body(self):
+        #return self.method.upper() in set(['POST', 'PUT'])
+        return True if (self.body_kwargs or self.body_args) else False
+        #return True if self.body_kwargs else False
+        #return self.method.upper() not in set(['GET'])
+
+    def get_auth_bearer(self):
+        """return the bearer token in the authorization header if it exists"""
+        access_token = ''
+        auth_header = self.get_header('authorization')
+        if auth_header:
+            m = re.search(r"^Bearer\s+(\S+)$", auth_header, re.I)
+            if m: access_token = m.group(1)
+
+        return access_token
+
+    def get_auth_basic(self):
+        """return the username and password of a basic auth header if it exists
+        """
+        username = ''
+        password = ''
+        auth_header = self.get_header('authorization')
+        if auth_header:
+            m = re.search(r"^Basic\s+(\S+)$", auth_header, re.I)
+            if m:
+                auth_str = Base64.decode(m.group(1))
+                username, password = auth_str.split(':', 1)
+
+        return username, password
+
+    def get_auth_scheme(self):
+        """The authorization header is defined like:
+
+            Authorization = credentials
+            credentials = auth-scheme TOKEN_VALUE
+            auth-scheme = token
+
+        which roughly translates to:
+
+            Authorization: token TOKEN_VALUE
+
+        This returns the token part of the auth header's value
+
+        :returns: string, the authentication scheme (eg, Bearer, Basic)
+        """
+        scheme = ""
+        auth_header = self.get_header('authorization')
+        if auth_header:
+            m = re.search(r"^(\S+)\s+", auth_header)
+            if m:
+                scheme = m.group(1)
+        return scheme
+
+    def is_auth(self, scheme):
+        """Return True if scheme matches the authorization scheme
+
+        :Example:
+            # Authorization: Basic FOOBAR
+            request.is_auth("basic") # True
+            request.is_auth("bearer") # False
+        """
+        return scheme.lower() == self.get_auth_scheme().lower()
+
+    def is_oauth(self, scheme):
+        """Similar to .is_auth() but checks for a wider range of names and also
+        will check for values like "client_id" and "client_secret" being passed
+        up in the body because javascript doesn't want to set headers in
+        websocket connections
+
+        :param scheme: string, the scheme you want to check, usually "basic" or
+            "bearer"
+        :return: boolean
+        """
+        scheme = scheme.lower()
+        if scheme in set(["bearer", "token", "access"]):
+            access_token = self.access_token
+            return True if access_token else False
+
+        elif scheme in set(["basic", "client"]):
+            client_id, client_secret = self.client_tokens
+            return True if (client_id and client_secret) else False
+
+
+class Response(Call):
+    """The Response object, every request instance that comes in will get a
+    corresponding Response instance that answers the Request.
+
+    an instance of this class is used to create the text response that will be
+    sent back to the client
+
+    Request has a ._body and .body, the ._body property is the raw value that is
+    returned from the Controller method that handled the request, the .body
+    property is a string that is ready to be sent back to the client, so it is
+    _body converted to a string. The reason _body isn't name body_kwargs is
+    because _body can be almost anything (not just a dict)
+    """
+    encoding = ""
+
+    error = None
+    """Will contain any raised exception"""
+
+    @cachedproperty(cached="_code", onget=False)
+    def code(self):
+        """the http status code to return to the client, by default, 200 if a
+        body is present otherwise 204"""
+        if self.has_body():
+            code = 200
+        else:
+            code = 204
+
+        return code
+
+    @code.setter
+    def code(self, v):
+        self._code = v
+        try:
+            del(self.status)
+
+        except AttributeError:
+            pass
+
+    @cachedproperty(cached="_code")
+    def status_code(self):
+        return self.code
+
+    @cachedproperty(cached="_status", onget=False)
+    def status(self):
+        """The full http status (the first line of the headers in a server
+        response)"""
+        return Status(self.code)
+
+    @cachedproperty(setter="_body")
+    def body(self, v):
+        self._body = v
+        if self.is_file():
+            filepath = getattr(v, "name", "")
+            if filepath:
+                mt = MimeType.find_type(filepath)
+                filesize = os.path.getsize(filepath)
+                self.set_header("Content-Type", mt)
+                self.set_header("Content-Length", filesize)
+                logger.debug(" ".join([
+                    f"Response body set to file: \"{filepath}\"",
+                    f"with mimetype: \"{mt}\"",
+                    f"and size: {filesize}",
+                ]))
+
+            else:
+                logger.warn(
+                    "Response body is a filestream that has no .filepath property"
+                )
+
+    def has_body(self):
+        """return True if there is an actual response body"""
+        return getattr(self, "_body", None) is not None
+
+    def is_file(self):
+        """return True if the response body is a file pointer"""
+        # http://stackoverflow.com/questions/1661262/check-if-object-is-file-like-in-python
+        return isinstance(getattr(self, "_body", None), io.IOBase)
+        #return hasattr(self._body, "read") if self.has_body() else False
+
+    def is_success(self):
+        """return True if this response is considered a "successful" response"""
+        code = self.code
+        return code < 400
+
+    def is_successful(self):
+        return self.is_success()
+
diff --git a/endpoints/client.py b/endpoints/client.py
index eebf2b2..a58cdb2 100644
--- a/endpoints/client.py
+++ b/endpoints/client.py
@@ -1,9 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import urllib
-import subprocess
-import json
-import os
 import re
 from contextlib import contextmanager
 import random
@@ -12,10 +7,13 @@
 import time
 import socket
 import logging
+import uuid
 
-import requests
-#from requests.auth import HTTPBasicAuth
-from requests.auth import _basic_auth_str
+try:
+    import requests
+    from requests.auth import _basic_auth_str
+except ImportError:
+    requests = None
 
 try:
     # https://github.com/websocket-client/websocket-client
@@ -23,20 +21,28 @@
 except ImportError:
     websocket = None
 
+from datatypes import Host
+
 from .compat import *
-from .utils import String, ByteString, Base64
-from .http import Headers, Url, Host
-from .interface import Payload
+from .utils import String, ByteString, Base64, Url
+from .interface.base import BaseApplication
+from .call import Headers
 
 
 logger = logging.getLogger(__name__)
 
 
-class WebClient(object):
-    """A generic test client that can make endpoint requests"""
+class HTTPClient(object):
+    """A generic test client that can make endpoints requests"""
     timeout = 10
 
     def __init__(self, host, *args, **kwargs):
+        if not requests:
+            logger.error("You need to install requests to use {}".format(
+                type(self).__name__
+            ))
+            raise ImportError("requests is not installed")
+
         self.host = Url(host, hostname=Host(host).client())
 
         # these are the common headers that usually don't change all that much
@@ -263,14 +269,13 @@ def set_version(self, version):
         )
 
 
-class WebsocketClient(WebClient):
+class WebSocketClient(HTTPClient):
     """a websocket client
 
     pretty much every method of this client can accept a timeout argument, if you
     don't include the timeout then self.timeout will be used instead
     """
-
-    payload_class = Payload
+    application_class = BaseApplication
 
     attempts = 3
     """how many times the client should attempt to connect/re-connect"""
@@ -289,10 +294,16 @@ def __init__(self, host, *args, **kwargs):
 
         kwargs.setdefault("headers", Headers())
         kwargs["headers"]['User-Agent'] = "{} Websocket Client".format(__name__.split(".")[0])
-        super(WebsocketClient, self).__init__(host, *args, **kwargs)
+        super().__init__(host, *args, **kwargs)
 
         self.set_trace(kwargs.pop("trace", False))
-        self.client_id = Base64.encode(String(id(self)))
+
+        # The value of [the Sec-WebSocket-Key] header field MUST be a nonce
+        # consisting of a randomly selected 16-byte value that has been
+        # base64-encoded.  The nonce MUST be selected randomly for each
+        # connection.
+        self.client_id = Base64.encode(uuid.uuid4().bytes)
+        #self.client_id = Base64.encode(String(id(self)))
         #self.client_id = Base64.encode(os.urandom(16))
         self.send_count = 0
         self.attempts = kwargs.pop("attempts", self.attempts)
@@ -321,7 +332,7 @@ def set_trace(self, trace):
             websocket.enableTrace(True) # for debugging connection issues
 
     def get_fetch_headers(self, method, headers):
-        headers = super(WebsocketClient, self).get_fetch_headers(method, headers)
+        headers = super().get_fetch_headers(method, headers)
         headers.setdefault("Sec-WebSocket-Key", self.client_id)
         return headers
 
@@ -357,7 +368,6 @@ def connect(self, path="", headers=None, query=None, timeout=0, **kwargs):
         timeout = self.get_timeout(timeout=timeout, **kwargs)
 
         self.set_trace(kwargs.pop("trace", False))
-        #pout.v(websocket_url, websocket_headers, self.query_kwargs, self.headers)
 
         try:
             logger.debug("{} connecting to {}".format(self.client_id, ws_url))
@@ -368,41 +378,44 @@ def connect(self, path="", headers=None, query=None, timeout=0, **kwargs):
                 sslopt={'cert_reqs':ssl.CERT_NONE},
             )
 
-            ret = self.recv_callback(callback=lambda r: r.uuid == ws_headers["Sec-Websocket-Key"])
+            ret = self.recv_callback(
+                callback=lambda r: r.uuid == ws_headers["Sec-Websocket-Key"]
+            )
             if ret.code >= 400:
                 raise IOError("Failed to connect with code {}".format(ret.code))
 
             logger.debug("{} connected to {}".format(self.client_id, ws_url))
 
         except websocket.WebSocketTimeoutException as e:
-            #pout.v(e)
-            raise IOError("Failed to connect within {} seconds".format(timeout))
+            raise IOError(
+                "Failed to connect within {} seconds".format(timeout)
+            ) from e
 
         except websocket.WebSocketException as e:
-            #pout.v(e)
-            raise IOError("Failed to connect with error: {}".format(e))
+            raise IOError(
+                "Failed to connect with error: {}".format(e)
+            ) from e
 
         except socket.error as e:
             # this is an IOError, I just wanted to be aware of that, most common
             # problem is: [Errno 111] Connection refused
-            #pout.v(e)
             raise
 
-#         except Exception as e:
-#             #pout.v(e)
-#             raise
-
         return ret
 
     def get_fetch_request(self, method, path, body, **kwargs):
         payload_body = self.get_fetch_query({})
         payload_body.update(self.get_fetch_body(body))
-        p = self.payload_class.dumps(dict(
+
+        kwargs.setdefault("uuid", self.client_id)
+
+        p = self.application_class.get_websocket_dumps(
             method=method.upper(),
             path=path,
             body=payload_body,
             **kwargs
-        ))
+        )
+
         return p
 
     def send(self, path, body, **kwargs):
@@ -448,16 +461,21 @@ def fetch(self, method, path, query=None, body=None, timeout=0, **kwargs):
                         kwargs['timeout'] = timeout
                         kwargs["attempt"] = attempt
 
-                        logger.debug('{} send {} attempt {}/{} with timeout {}'.format(
-                            self.client_id,
-                            uuid,
-                            attempt,
-                            max_attempts,
-                            timeout
-                        ))
+                        logger.debug(
+                            '{} send {} attempt {}/{} with timeout {}'.format(
+                                self.client_id,
+                                uuid,
+                                attempt,
+                                max_attempts,
+                                timeout
+                            )
+                        )
 
                         sent_bits = self.ws.send(payload)
-                        logger.debug('{} sent {} bytes'.format(self.client_id, sent_bits))
+                        logger.debug('{} sent {} bytes'.format(
+                            self.client_id,
+                            sent_bits
+                        ))
                         if sent_bits:
                             ret = self.fetch_response(uuid, **kwargs)
                             if ret:
@@ -465,37 +483,46 @@ def fetch(self, method, path, query=None, body=None, timeout=0, **kwargs):
 
                 except websocket.WebSocketConnectionClosedException as e:
                     self.ws.shutdown()
-                    raise IOError("connection is not open but reported it was open: {}".format(e))
 
             except (IOError, TypeError) as e:
-                logger.debug('{} error on send attempt {}: {}'.format(self.client_id, attempt, e))
+                logger.debug('{} error on send attempt {}: {}'.format(
+                    self.client_id,
+                    attempt,
+                    e
+                ))
                 success = False
 
             finally:
                 if not success:
                     attempt += 1
                     if attempt > max_attempts:
-                        raise RuntimeError("{} fetch attempts exceeded {} max attempts".format(attempt, max_attempts))
+                        raise RuntimeError(
+                            "{} fetch attempts exceeded {} max attempts".format(
+                                attempt,
+                                max_attempts
+                            )
+                        )
 
                     else:
                         timeout *= 2
                         if (attempt / max_attempts) > 0.50:
-                            logger.debug(
-                                "{} closing and re-opening connection for next attempt".format(self.client_id)
-                            )
+                            logger.debug(" ".join([
+                                f"{self.client_id} closing and re-opening",
+                                "connection for next attempt",
+                            ]))
                             self.close()
 
         return ret
 
     def fetch_response(self, uuid, **kwargs):
-        """payload has been sent, do anything else you need to do (eg, wait for response?)
+        """payload has been sent, do anything else you need to do (eg, wait for
+        response?)
 
-        :param uuid: string, the unique identifier of a websocket request we are waiting for the response to
+        :param uuid: string, the unique identifier of a websocket request we are
+        waiting for the response to
         :returns: mixed, the response payload
         """
         def callback(res_payload):
-            #pout.v(req_payload, res_payload)
-            #ret = req_payload.uuid == res_payload.uuid or res_payload.uuid == "CONNECT"
             ret = res_payload.uuid == uuid
             if ret:
                 logger.debug('{} received {} response for {}'.format(
@@ -517,21 +544,34 @@ def rand_id(size=8, chars=string.ascii_uppercase + string.digits):
 
         payload = rand_id()
         self.ws.ping(payload)
-        opcode, data = self.recv_raw(timeout, [websocket.ABNF.OPCODE_PONG], **kwargs)
+        opcode, data = self.recv_raw(
+            timeout,
+            [websocket.ABNF.OPCODE_PONG],
+            **kwargs
+        )
         if data != payload:
             raise IOError("Pinged server but did not receive correct pong")
 
     def recv_raw(self, timeout, opcodes, **kwargs):
         """this is very internal, it will return the raw opcode and data if they
-        match the passed in opcodes"""
+        match the passed in opcodes
+
+        You can find the opcode values here:
+            https://github.com/websocket-client/websocket-client/blob/master/websocket/_abnf.py
+        """
         orig_timeout = self.get_timeout(timeout)
         timeout = orig_timeout
 
         while timeout > 0.0:
             start = time.time()
-            if not self.connected: self.connect(timeout=timeout, **kwargs)
+            if not self.connected:
+                self.connect(timeout=timeout, **kwargs)
+
             with self.wstimeout(timeout, **kwargs) as timeout:
-                logger.debug('{} waiting to receive for {} seconds'.format(self.client_id, timeout))
+                logger.debug('{} waiting to receive for {} seconds'.format(
+                    self.client_id,
+                    timeout
+                ))
                 try:
                     opcode, data = self.ws.recv_data()
                     if opcode in opcodes:
@@ -546,14 +586,17 @@ def recv_raw(self, timeout, opcodes, **kwargs):
                     pass
 
                 except websocket.WebSocketConnectionClosedException:
-                    # bug in Websocket.recv_data(), this should be done by Websocket
+                    # bug in Websocket.recv_data(), this should be done by
+                    # Websocket
                     try:
                         self.ws.shutdown()
+
                     except AttributeError:
                         pass
-                    #raise EOFError("websocket closed by server and reconnection did nothing")
 
-            if timeout:
+                    raise
+
+            if timeout > 0.0:
                 stop = time.time()
                 timeout -= (stop - start)
 
@@ -569,25 +612,27 @@ def get_fetch_response(self, raw):
         """This just makes the payload instance more HTTPClient like"""
         class Return(object): pass
         ret = Return()
-        p = self.payload_class.loads(raw)
+        p = self.application_class.get_websocket_loads(raw)
 
         for k, v in p.items():
             setattr(ret, k, v)
-        #p.code = p["code"]
-        #p._body = p["body"]
         ret._body = ret.body
         return ret
 
     def recv(self, timeout=0, **kwargs):
-        """this will receive data and convert it into a message, really this is more
-        of an internal method, it is used in recv_callback and recv_msg"""
-        opcode, data = self.recv_raw(timeout, [websocket.ABNF.OPCODE_TEXT], **kwargs)
+        """this will receive data and convert it into a message, really this is
+        more of an internal method, it is used in recv_callback and recv_msg"""
+        opcode, data = self.recv_raw(
+            timeout,
+            [websocket.ABNF.OPCODE_TEXT],
+            **kwargs
+        )
         return self.get_fetch_response(data)
 
     def recv_callback(self, callback, **kwargs):
         """receive messages and validate them with the callback, if the callback 
-        returns True then the message is valid and will be returned, if False then
-        this will try and receive another message until timeout is 0"""
+        returns True then the message is valid and will be returned, if False
+        then this will try and receive another message until timeout is 0"""
         payload = None
         timeout = self.get_timeout(**kwargs)
         full_timeout = timeout
@@ -607,9 +652,6 @@ def recv_callback(self, callback, **kwargs):
 
         return payload
 
-#     def has_connection(self):
-#         return self.connected
-
     def close(self):
         if self.connected:
             self.ws.close()
diff --git a/endpoints/compat.py b/endpoints/compat.py
index ca77d52..6e1a9e4 100644
--- a/endpoints/compat.py
+++ b/endpoints/compat.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 
 from datatypes.compat import *
 from datatypes import String, ByteString
diff --git a/endpoints/config.py b/endpoints/config.py
new file mode 100644
index 0000000..47f4162
--- /dev/null
+++ b/endpoints/config.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+
+from datatypes.config import (
+    Environ as BaseEnviron,
+)
+
+
+class Environ(BaseEnviron):
+    def __init__(self, *args, **kwargs):
+        super().__init__(namespace="ENDPOINTS_", **kwargs)
+
+        # Default encoding
+        self.setdefault("ENCODING", "UTF-8")
+
+        # the response content type a controller will set
+        self.setdefault("RESPONSE_CONTENT_TYPE", "application/json")
+
+        # The host string, usually just domain or domain:port, this is used by
+        # the server classes and also the tests
+        self.setdefault("HOST", "")
+
+    def set_host(self, host):
+        self.set("HOST", host)
+
+    def set_controller_prefixes(self, prefixes, env_name='ENDPOINTS_PREFIX'):
+        """set the controller_prefixes found in env_name to prefixes, this will
+        remove any existing found controller prefixes 
+
+        :param prefixes: list, the new prefixes that will replace any old prefixes
+        :param env_name: string, the name of the environment variables
+        """
+        self.nset(env_name, prefixes)
+
+    def get_prefix_names(self, env_name):
+        """This returns the actual environment variable names from * -> *_N
+
+        :param env_name: string, the name of the environment variables
+        :returns: generator, the found environment names
+        """
+        for k in self.nkeys(env_name):
+            yield k
+
+    def get_controller_prefixes(self, env_name='ENDPOINTS_PREFIX'):
+        """this will look for ENDPOINTS_PREFIX, and ENDPOINTS_PREFIX_N (where
+        N is 1 to infinity) in the environment, if it finds them, it will assume they
+        are python module paths where endpoints can find Controller subclasses
+
+        The num checks (eg ENDPOINTS_PREFIX_1, ENDPOINTS_PREFIX_2) go in order, so you
+        can't do ENDPOINTS_PREFIX_1, ENDPOINTS_PREFIX_3, because it will fail on _2
+        and move on, so make sure your num dsns are in order (eg, 1, 2, 3, ...)
+
+        :Example:
+            export ENDPOINTS_PREFIX_1=foo.controllers
+            export ENDPOINTS_PREFIX_2=bar.che
+            $ python
+            >>> from endpoints import environ
+            >>> environ.get_controller_prefixes
+            ['foo.controller', 'bar.che']
+
+        :param env_name: string, the name of the environment variables
+        :returns: list, the found module paths
+        """
+        return list(self.paths(env_name))
+
+
+environ = Environ()
+
diff --git a/endpoints/decorators/__init__.py b/endpoints/decorators/__init__.py
index 29338f1..679c557 100644
--- a/endpoints/decorators/__init__.py
+++ b/endpoints/decorators/__init__.py
@@ -1,16 +1,12 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 
-from decorators import FuncDecorator
-
-from .auth import (
-    auth,
-    auth_basic,
-    auth_client,
-    auth_token,
+from .base import (
+    BackendDecorator,
+    ControllerDecorator,
 )
 
 from .limit import (
+    RateLimitBackend,
     RateLimitDecorator,
     ratelimit_ip,
     ratelimit_access_token,
@@ -18,26 +14,27 @@
     ratelimit_param_ip,
 )
 
-from .base import (
-    TargetDecorator,
-    BackendDecorator,
-    ControllerDecorator,
-)
-
-from .call import (
-    route,
-    route_path,
-    route_param,
-    version
+from .auth import (
+    AuthBackend,
+    AuthDecorator,
+    auth_basic,
+    auth_client,
+    auth_token,
 )
 
 from .utils import (
     httpcache,
     nohttpcache,
-    property,
+    code_error,
     param,
     param_query,
     param_body,
-    code_error,
+)
+
+from .call import (
+    route,
+    route_path,
+    route_param,
+    version
 )
 
diff --git a/endpoints/decorators/auth.py b/endpoints/decorators/auth.py
index e2a8517..e35cfb8 100644
--- a/endpoints/decorators/auth.py
+++ b/endpoints/decorators/auth.py
@@ -1,16 +1,34 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import logging
 
 from ..exception import CallError, AccessDenied
 from ..utils import String
-from .base import TargetDecorator
+from .base import BackendDecorator
 
 
 logger = logging.getLogger(__name__)
 
 
-class auth(TargetDecorator):
+class AuthBackend(object):
+    async def handle(self, method_name, **kwargs):
+        callback = getattr(self, method_name)
+        return callback(**kwargs)
+
+    async def auth_basic(self, controller, username, password):
+        raise NotImplementedError()
+
+    async def auth_client(self, controller, client_id, client_secret):
+        return await self.auth_basic(
+            controller=controller,
+            username=client_id,
+            password=client_secret,
+        )
+
+    async def auth_token(self, controller, access_token):
+        raise NotImplementedError()
+
+
+class AuthDecorator(BackendDecorator):
     """
     handy auth decorator that makes doing basic or token auth easy peasy
 
@@ -41,24 +59,35 @@ def GET(self):
     realm = ""
     """Optional namespace for WWW-Authenticate header"""
 
-    def handle_error(self, controller, e):
-        logger.debug(e, exc_info=True)
-        if isinstance(e, NotImplementedError):
-            raise CallError(403, "You need a validator function to use authentication")
+    backend_class = AuthBackend
+
+    async def handle_kwargs(self, controller, controller_args, controller_kwargs):
+        return {
+            "method_name": self.__class__.__name__,
+            "controller": controller,
+            "controller_args": controller_args,
+            "controller_kwargs": controller_kwargs,
+        }
+
+    async def handle_error(self, controller, e):
+        if isinstance(e, CallError):
+            super().handle_error(controller, e)
+
+        elif isinstance(e, NotImplementedError):
+            raise CallError(
+                403,
+                "You need a validator function to use authentication"
+            ) from e
+
         else:
             raise AccessDenied(
                 String(e),
                 scheme=self.scheme,
                 realm=self.realm,
-            )
+            ) from e
 
-    def definition(self, target=None, *anoop, **kwnoop):
-        """makes target optional since auth decorators are made to have target passed
-        in or to easily be extended"""
-        return super(auth, self).definition(target=target)
 
-
-class auth_basic(auth):
+class auth_basic(AuthDecorator):
     """
     handy basic auth decorator that checks for username, password in an auth header
 
@@ -78,13 +107,21 @@ def GET(self):
     """
     scheme = AccessDenied.SCHEME_BASIC
 
-    def handle_args(self, controller, controller_args, controller_kwargs):
+    async def handle_kwargs(self, controller, controller_args, controller_kwargs):
         username, password = controller.request.get_auth_basic()
 
-        if not username: raise ValueError("username is required")
-        if not password: raise ValueError("password is required")
+        if not username:
+            raise ValueError("username is required")
+
+        if not password:
+            raise ValueError("password is required")
 
-        return [controller, username, password]
+        return {
+            "method_name": "auth_basic",
+            "controller": controller,
+            "username": username,
+            "password": password,
+        }
 
 
 class auth_client(auth_basic):
@@ -105,16 +142,24 @@ class Default(Controller):
             def GET(self):
                 return "hello world"
     """
-    def handle_args(self, controller, controller_args, controller_kwargs):
+    async def handle_kwargs(self, controller, controller_args, controller_kwargs):
         client_id, client_secret = controller.request.client_tokens
 
-        if not client_id: raise ValueError("client_id is required")
-        if not client_secret: raise ValueError("client_secret is required")
+        if not client_id:
+            raise ValueError("client_id is required")
+
+        if not client_secret:
+            raise ValueError("client_secret is required")
 
-        return [controller, client_id, client_secret]
+        return {
+            "method_name": "auth_client",
+            "controller": controller,
+            "client_id": client_id,
+            "client_secret": client_secret,
+        }
 
 
-class auth_token(auth):
+class auth_token(AuthDecorator):
     """
     handy token auth decorator that checks for access_token in an authorization
     Bearer header
@@ -135,10 +180,15 @@ def GET(self):
     """
     scheme = AccessDenied.SCHEME_BEARER
 
-    def handle_args(self, controller, controller_args, controller_kwargs):
+    async def handle_kwargs(self, controller, controller_args, controller_kwargs):
         access_token = controller.request.access_token
 
-        if not access_token: raise ValueError("access_token is required")
+        if not access_token:
+            raise ValueError("access_token is required")
 
-        return [controller, access_token]
+        return {
+            "method_name": "auth_token",
+            "controller": controller,
+            "access_token": access_token,
+        }
 
diff --git a/endpoints/decorators/base.py b/endpoints/decorators/base.py
index 5947943..1163108 100644
--- a/endpoints/decorators/base.py
+++ b/endpoints/decorators/base.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import logging
+import asyncio
 
-from decorators import FuncDecorator
+from datatypes.decorators import FuncDecorator
 
 from ..compat import *
 from ..exception import CallError
@@ -12,48 +12,83 @@
 
 
 class ControllerDecorator(FuncDecorator):
-    """Base decorator providing common functionality to run .handle() when a decorated
-    function is called, this class is meant to be extended by a child
+    """Base decorator providing common functionality to run .handle() when a
+    decorated function is called, this class is meant to be extended by a child
 
     This also is Controller specific, it's meant to be used in decorators that
     decorate Controller methods
 
     .. seealso:: decorators.auth
     """
-    def handle_args(self, controller, controller_args, controller_kwargs):
-        """Returns the *args part that will be passed into the .handle() method
+    def decorate(self, func, *args, **kwargs):
+        """decorate the passed in func calling target when func is called
 
-        this is called from .handle_params()
+        You should never override this method unless you know what you are doing
+
+        :param func: callable, the function being decorated
+        :param *args: these are the positional arguments passed into the
+            decorator __init__ method
+        :param **kwargs: these are the named arguments passed into the decorator
+            __init__ method
+        :returns: the decorated func
         """
-        return [controller, controller_args, controller_kwargs]
+        self.definition(*args, **kwargs)
+
+        async def decorated(controller, *controller_args, **controller_kwargs):
+            await self.handle_call(
+                controller,
+                controller_args,
+                controller_kwargs
+            )
+
+            return await self.handle_controller(
+                func,
+                controller,
+                controller_args,
+                controller_kwargs,
+            )
+
+        return decorated
+
+    def definition(self, *args, **kwargs):
+        """whatever is passed into the decorator creation will be passed to this
+        method, so you can set instance variables and stuff
 
-    def handle_kwargs(self, controller, controller_args, controller_kwargs):
+        This can be overridden by child classes but this cannot be made async
+        and it should avoid doing any IO because of that
+
+        :Example:
+            a decorator like:
+                @foo("bar", "che", baz=1)
+                def function_name(*args, **kwargs): pass
+
+            would be passed to this method as:
+
+                definition(*("bar", "che"), **{"baz": 1})
+
+
+        :param *args: these are the positional arguments passed into the
+            decorator __init__ method
+        :param **kwargs: these are the named arguments passed into the decorator
+            __init__ method
+        """
+        self.decorator_args = args
+        self.decorator_kwargs = kwargs
+
+    async def handle_kwargs(self, controller, controller_args, controller_kwargs):
         """Returns the **kwargs part that will be passed into the .handle() method
 
         this is called from .handle_params()
         """
-        return {}
-
-    def handle_params(self, controller, controller_args, controller_kwargs):
-        """get params ready for calling .handle()
-
-        this method exists because child classes might only really need certain params
-        passed to the method, this allows the child classes to decided what their
-        .handle method needs
-
-        :param controller: call.Controller instance, you can get the request with
-            controller.request
-        :param controller_args: the arguments that will be passed to the controller
-        :param controller_kwargs: the key/val arguments that will be passed to the 
-            controller, these usually come from query strings and post bodies
-        :returns: a tuple (list, dict) that correspond to the *args, **kwargs that
-            will be passed to the target() method
-        """
-        args = [controller, controller_args, controller_kwargs]
-        return self.handle_args(*args), self.handle_kwargs(*args)
+        return {
+            "controller": controller,
+            "controller_args": controller_args,
+            "controller_kwargs": controller_kwargs,
+        }
 
-    def handle_error(self, controller, e):
-        """Any error the class isn't sure how to categorize will go through this method
+    async def handle_error(self, controller, e):
+        """Any error the class isn't sure how to categorize will go through this
+        method
 
         overriding this method allows child classes to customize responses based
         on certain encountered errors
@@ -62,114 +97,86 @@ def handle_error(self, controller, e):
             raised e
         :param e: the raised error
         """
-        logger.warning(e, exc_info=True)
+        if not isinstance(e, CallError):
+            logger.warning(e)
+
         raise e
 
-    def handle_call(self, controller, controller_args, controller_kwargs):
+    async def handle_controller(self, func, controller, controller_args, controller_kwargs):
+        ret = func(controller, *controller_args, **controller_kwargs)
+        while asyncio.iscoroutine(ret):
+            ret = await ret
+
+        return ret
+
+    async def handle_call(self, controller, controller_args, controller_kwargs):
         """Internal method for this class
 
         handles normalizing the passed in values from the decorator using
         .handle_params() and then passes them to .handle()
         """
         try:
-            handle_args, handle_kwargs = self.handle_params(
-                controller,
-                controller_args,
-                controller_kwargs
+            handle_kwargs = await self.handle_kwargs(
+                controller=controller,
+                controller_args=controller_args,
+                controller_kwargs=controller_kwargs
             )
-            ret = self.handle(*handle_args, **handle_kwargs)
-            if ret is not None and not ret:
-                raise ValueError("{} check failed".format(self.__class__.__name__))
-
-        except CallError:
-            raise
-
-        except Exception as e:
-            self.handle_error(controller, e)
-
-    def decorate(self, func, *args, **kwargs):
-        """decorate the passed in func calling target when func is called
-
-        In child classes this will most likely be 
-
-        :param func: the function being decorated
-        :param target: the target that will be run when func is called
-        :returns: the decorated func
-        """
-        self.definition(*args, **kwargs)
-
-        def decorated(controller, *controller_args, **controller_kwargs):
-            self.handle_call(controller, controller_args, controller_kwargs)
-            return func(controller, *controller_args, **controller_kwargs)
-
-        return decorated
-
-    def definition(self, *args, **kwargs):
-        """whatever is passed into the decorator creation will be passed to this
-        method, so you can set instance variables and stuff, this is meant to 
-        be overridden in child classes
-
-        :Example:
-            a decorator like:
-                @foo("bar", "che", baz=1)
-                def function_name(*args, **kwargs): pass
-
-            would be passed to this method as:
 
-                definition(*["bar", "che"], **{"baz": 1})
+            ret = self.handle(**handle_kwargs)
+            while asyncio.iscoroutine(ret):
+                ret = await ret
 
+            if ret is not None and not ret:
+                raise ValueError(
+                    "{} check failed".format(self.__class__.__name__)
+                )
 
-        :param *args: list, should be defined in a child class
-        :param **kwargs: dict, all the passed in kwargs
-        """
-        self.args = args
-        self.kwargs = kwargs
+        except Exception as e:
+            await self.handle_error(controller, e)
 
-    def handle(self, *args, **kwargs):
+    async def handle(self, *args, **kwargs):
         """The meat of the decorator, this is where all the functionality should
         go in the child class, this is meant to be extended
 
         if this raises an error it will be passed to .handle_error()
 
-        :param *args: list, whatever returned from .handle_params()[0]
-        :param **kwargs: dict, whatever returned from .handle_params()[1]
+        :param **kwargs: dict, whatever returned from .normalize_handle_kwargs()
+        :returns: bool, if this method returns False then it will cause a
+            ValueError to be raised signalling the input failed this decorator
         """
-        raise NotImplementedError()
-
+        return True
 
-class TargetDecorator(ControllerDecorator):
-    """Base decorator providing common functionality to run a target when decorated
-    function is called, this class is meant to be extended by a child
 
-    This also is very Controller specific, it's meant to be used in decorators that
-    decorate Controller methods
-
-    .. seealso:: decorators.auth
-    """
-    def definition(self, target, *anoop, **kwnoop):
-        if target:
-            self.target = target
+class BackendDecorator(ControllerDecorator):
 
-    def handle(self, *args, **kwargs):
-        return self.target(*args, **kwargs)
+    backend_class = None
 
+    async def get_backend(self, *args, **kwargs):
+        backend = getattr(self, "backend", None)
+        if backend is None:
+            self.backend = backend = await self.create_backend(*args, **kwargs)
 
-class BackendDecorator(ControllerDecorator):
+        return backend
 
-    backend_class = None
+    async def create_backend(self, *args, **kwargs):
+        backend = kwargs.pop("backend", None)
 
-    def create_backend(self, *args, **kwargs):
-        if not self.backend_class:
-            self.backend_class = kwargs.pop("backend_class", None)
+        if not backend:
+            backend_class = kwargs.pop("backend_class", self.backend_class)
 
-        if not self.backend_class:
-            raise ValueError("You are using a BackendDecorator with no backend class")
+            if not backend_class:
+                raise ValueError(
+                    "You are using a BackendDecorator with no backend class"
+                )
 
-        return self.backend_class(*args, **kwargs)
+            backend = backend_class(*args, **kwargs)
 
-    def handle(self, *args, **kwargs):
-        return self.backend.handle(*args, **kwargs)
+        return backend
 
-    def definition(self, *args, **kwargs):
-        self.backend = self.create_backend(*args, **kwargs)
+    async def handle(self, *args, **kwargs):
+        backend = await self.get_backend(
+            *self.decorator_args,
+            **self.decorator_kwargs,
+        )
+        return backend.handle(*args, **kwargs)
 
diff --git a/endpoints/decorators/call.py b/endpoints/decorators/call.py
index c3da32b..f5a7077 100644
--- a/endpoints/decorators/call.py
+++ b/endpoints/decorators/call.py
@@ -1,11 +1,8 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import logging
-import inspect
-import re
 
 from ..exception import CallError, RouteError, VersionError
-from ..http import Url
+from ..utils import Url
 from .base import ControllerDecorator
 
 
@@ -13,7 +10,8 @@
 
 
 class route(ControllerDecorator):
-    """Used to decide if the Controller's method should be used to satisfy the request
+    """Used to decide if the Controller's method should be used to satisfy the
+    request
 
     :example:
         class Default(Controller):
@@ -27,23 +25,25 @@ def GET_1(self, uid, title):
             def GET_2(self, username):
                 pass
 
-    If this decorator is used then all GET methods in the controller have to have
-    a unique name (ie, there can be no just GET method, they have to be GET_1, etc.)
+    If this decorator is used then all GET methods in the controller have to
+    have a unique name (ie, there can be no just GET method, they have to be
+    GET_1, etc.)
     """
     def definition(self, callback, *args, **kwargs):
         self.callback = callback
         self.error_code = kwargs.pop("error_code", 405)
 
-    def handle(self, request):
-        return self.callback(request)
+    async def handle(self, controller, **kwargs):
+        return self.callback(controller.request)
 
-    def handle_args(self, controller, controller_args, controller_kwargs):
-        return [controller.request]
+    async def handle_error(self, controller, e):
+        if isinstance(e, CallError):
+            super().handle_error(controller, e)
 
-    def handle_error(self, controller, e):
-        raise RouteError(instance=self)
+        else:
+            raise RouteError(instance=self) from e
 
-    def handle_failure(self, controller):
+    async def handle_failure(self, controller):
         """This is called if all routes fail, it's purpose is to completely
         fail the request
 
@@ -56,20 +56,23 @@ def handle_failure(self, controller):
         :param controller: Controller, the controller that was trying to find a 
             method to route to
         """
-        req = controller.request
+        request = controller.request
 
         # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
         # An origin server SHOULD return the status code 405 (Method Not Allowed)
         # if the method is known by the origin server but not allowed for the
         # requested resource
-        raise CallError(self.error_code, "Could not find a method to satisfy {}".format(
-            req.path
-        ))
+        raise CallError(
+            self.error_code,
+            "Could not find a method to satisfy {}".format(
+                request.path
+            )
+        )
 
 
 class route_path(route):
-    """easier route decorator that will check the sub paths to make sure they are part 
-    of the full endpoint path
+    """easier route decorator that will check the sub paths to make sure they
+    are part of the full endpoint path
 
     :Example:
 
@@ -83,8 +86,9 @@ def definition(self, *paths, **kwargs):
         self.paths = paths
         self.error_code = kwargs.pop("error_code", 404)
 
-    def handle(self, request):
+    async def handle(self, controller, **kwargs):
         ret = True
+        request = controller.request
         pas = Url.normalize_paths(self.paths)
         method_args = request.controller_info["method_args"]
         for i, p in enumerate(pas):
@@ -101,8 +105,8 @@ def handle(self, request):
 
 
 class route_param(route):
-    """easier route decorator that will check the parameters to make sure they are part 
-    of the request
+    """easier route decorator that will check the parameters to make sure they
+    are part of the request
 
     :Example:
 
@@ -119,8 +123,9 @@ def definition(self, *keys, **matches):
         # we throw a 400 here to match @param failures
         self.error_code = 400
 
-    def handle(self, request):
+    async def handle(self, controller, **kwargs):
         ret = True
+        request = controller.request
         method_kwargs = request.controller_info["method_kwargs"]
         for k in self.keys:
             if k not in method_kwargs:
@@ -156,20 +161,18 @@ def GET_1(self):
             def GET_2(self):
                 pass
 
-    If this decorator is used then all GET methods in the controller have to have
-    a unique name (ie, there can be no just GET method, they have to be GET_1, etc.)
+    If this decorator is used then all GET methods in the controller have to
+    have a unique name (ie, there can be no just GET method, they have to be
+    GET_1, etc.)
     """
     def definition(self, *versions, **kwargs):
         self.versions = set(versions)
         self.error_code = kwargs.pop("error_code", 404)
 
-    def handle_args(self, controller, controller_args, controller_kwargs):
-        return [controller]
+    async def handle_error(self, controller, e):
+        raise e
 
-    def handle_error(self, controller, e):
-        raise
-
-    def handle(self, controller):
+    async def handle(self, controller, **kwargs):
         req_version = controller.request.version(controller.content_type)
         if req_version not in self.versions:
             raise VersionError(self, req_version, self.versions)
diff --git a/endpoints/decorators/limit.py b/endpoints/decorators/limit.py
index 5d6fc96..2ab11aa 100644
--- a/endpoints/decorators/limit.py
+++ b/endpoints/decorators/limit.py
@@ -1,12 +1,11 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import logging
 import datetime
 
 from datatypes import Pool
 
 from ..compat import *
-from ..exception import CallError, AccessDenied
+from ..exception import CallError
 from ..utils import String
 from .base import BackendDecorator
 
@@ -15,14 +14,17 @@
 
 
 class RateLimitBackend(object):
-    """You can extend this backend and override the .handle() method to customize
-    rate limiting in your application
+    """You can extend this backend and override the .handle() method to
+    customize rate limiting in your application
 
     you can of course create your own Backend and just set it on the base limit
     decorator to use that backend for all your limiting
 
     :example:
-        from endpoints.decorators.limit import RateLimitDecorator, RateLimitBackend
+        from endpoints.decorators.limit import (
+            RateLimitDecorator,
+            RateLimitBackend,
+        )
 
         class MyBackend(RateLimitBackend):
             def handle(self, request, key, limit, ttl):
@@ -31,10 +33,7 @@ def handle(self, request, key, limit, ttl):
         RateLimitDecorator.backend_class = MyBackend
         # now all rate limiting will use MyBackend
     """
-    DEFAULT_LIMIT = 0
-    DEFAULT_TTL = 0
-
-    def handle(self, request, key, limit, ttl):
+    async def handle(self, request, key, limit, ttl):
         raise NotImplementedError()
 
 
@@ -46,7 +45,7 @@ class Backend(RateLimitBackend):
     _calls = Pool(5000)
     """class dictionary that will hold all limiting keys"""
 
-    def handle(self, request, key, limit, ttl):
+    async def handle(self, request, key, limit, ttl):
         now = datetime.datetime.utcnow()
         count = 1
 
@@ -58,7 +57,9 @@ def handle(self, request, key, limit, ttl):
                 td = now - calls[key]["date"]
                 if td.total_seconds() < ttl:
                     raise ValueError(
-                        "Please wait {} seconds to make another request".format(ttl - td.seconds)
+                        "Please wait {} seconds to make another request".format(
+                            ttl - td.seconds
+                        )
                     )
 
                 else:
@@ -73,12 +74,30 @@ def handle(self, request, key, limit, ttl):
 
 
 class RateLimitDecorator(BackendDecorator):
-    """Base decorator providing common functionality to rate limit a given controller
-    method
+    """Base decorator providing common functionality to rate limit a given
+    controller method
     """
     backend_class = Backend
 
-    def normalize_key(self, request, *args, **kwargs):
+    def definition(self, limit=0, ttl=0, path_in_key=True, *args, **kwargs):
+        """The definition of the decorator, this is called from the decorator's
+        __init__ method and is responsible for validating the passed in
+        arguments for the decorator
+
+        :param limit: int, max requests that can be received in ttl
+        :param ttl: int, how many seconds the request should be throttled
+            (eg, 3600 = 1 hour)
+        :param path_in_key: bool, True if you would like to include the request
+            path in the key, if False then the path will not be included so the
+            paths will be more global
+        """
+        self.limit = int(limit)
+        self.ttl = int(ttl)
+        self.path_in_key = path_in_key
+
+        super().definition(*args, **kwargs)
+
+    async def normalize_key(self, request, **kwargs):
         """Decide what key this request should have
 
         :example:
@@ -90,91 +109,57 @@ def normalize_key(self, request, *args, **kwargs):
         """
         raise NotImplementedError()
 
-    def normalize_limit(self, request, *args, **kwargs):
-        """Called with each request, if you would like to customize limit depending
-        on the request, this is the method to override
-
-        :param request: Request, the request instance
-        :returns: int, the desired limit for the request
-        """
-        return self.limit
-
-    def normalize_ttl(self, request, *args, **kwargs):
-        """Called with each request, if you would like to customize ttl depending
-        on the request, this is the method to override
-
-        :param request: Request, the request instance
-        :returns: int, the desired ttl for the request
-        """
-        return self.ttl
-
-    def handle_args(self, controller, controller_args, controller_kwargs):
+    async def handle_kwargs(self, controller, controller_args, controller_kwargs):
         """These arguments will be passed to the .handle() method"""
         request = controller.request
 
-        key = self.normalize_key(
+        key = await self.normalize_key(
             request,
             controller_args=controller_args,
             controller_kwargs=controller_kwargs,
         )
 
-        limit = self.normalize_limit(
-            request,
-            controller_args=controller_args,
-            controller_kwargs=controller_kwargs,
-        )
-
-        ttl = self.normalize_ttl(
-            request,
-            controller_args=controller_args,
-            controller_kwargs=controller_kwargs,
-        )
-
-        return request, key, limit, ttl
+        return {
+            "request": request,
+            "key": key,
+            "limit": self.limit,
+            "ttl": self.ttl,
+        }
 
-    def handle(self, request, key, limit, ttl):
+    async def handle(self, request, key, limit, ttl):
         """this will only run the request if the key has a value, if you want to
         fail if the key doesn't have a value, then normalize_key() should raise
         an exception
 
         :param request: Request, the request instance
-        :param key: string, the unique key for the endpoint, this is generated using
-            self.normalize_key, so override that method to customize the key
-        :param limit: int, max requests that should be received in ttl
-        :param ttl: int, how many seconds the request should be throttled (eg, 3600 = 1 hour)
+        :param key: string, the unique key for the endpoint, this is generated
+            using self.normalize_key, so override that method to customize the
+            key
+        :param limit: int, max requests can be received in ttl
+        :param ttl: int, how many seconds the request should be throttled
+            (eg, 3600 = 1 hour)
         """
         ret = True
         if key:
-            ret = super(RateLimitDecorator, self).handle(request, key, limit, ttl)
+            ret = await super().handle(request, key, limit, ttl)
+
         else:
-            logger.warn("No ratelimit key found for {}".format(request.path))
+            logger.warning("No ratelimit key found for {}".format(request.path))
 
         return ret
 
-    def handle_error(self, controller, e):
+    async def handle_error(self, controller, e):
         """all exceptions should generate 429 responses"""
-        raise CallError(429, String(e))
+        if isinstance(e, CallError):
+            await super().handle_error(controller, e)
 
-    def definition(self, limit=0, ttl=0, path_in_key=True, *args, **kwargs):
-        """The definition of the decorator, this is called from the decorator's __init__
-        method and is responsible for validating the passed in arguments for the decorator
-
-        :param limit: int, max requests that should be received in ttl
-        :param ttl: int, how many seconds the request should be throttled (eg, 3600 = 1 hour)
-        :param path_in_key: bool, True if you would like to include the request path
-            in the key, if False then the path will not be included so the paths
-            will be more global
-        """
-        super(RateLimitDecorator, self).definition(*args, **kwargs)
-
-        self.limit = int(limit) or getattr(self.backend, "DEFAULT_LIMIT", 0) 
-        self.ttl = int(ttl) or getattr(self.backend, "DEFAULT_TTL", 0)
-        self.path_in_key = path_in_key
+        else:
+            raise CallError(429, String(e)) from e
 
 
 class ratelimit_ip(RateLimitDecorator):
     """Rate limit by the client's ip address"""
-    def normalize_key(self, request, *args, **kwargs):
+    async def normalize_key(self, request, **kwargs):
         if self.path_in_key:
             ret = "{}{}".format(request.ip, request.path)
         else:
@@ -183,45 +168,66 @@ def normalize_key(self, request, *args, **kwargs):
 
 
 class ratelimit_access_token(RateLimitDecorator):
-    """Limit by the requested client's access token, because certain endpoints can
-    only be requested a certain amount of times for the given access token"""
-    def normalize_key(self, request, *args, **kwargs):
+    """Limit by the requested client's access token, because certain endpoints
+    can only be requested a certain amount of times for the given access token
+    """
+    async def normalize_key(self, request, **kwargs):
         if self.path_in_key:
             ret = "{}{}".format(request.access_token, request.path)
+
         else:
             ret = request.access_token
+
         return ret
 
 
 class ratelimit_param(RateLimitDecorator):
-    """this will limit on a parameter value. So, for example, if you want to limit
-    login attempts for an email address you would pass in "email" to this decorator"""
-    def normalize_key(self, request, controller_args, controller_kwargs):
+    """this will limit on a parameter value. So, for example, if you want to
+    limit login attempts for an email address you would pass in "email" to this
+    decorator
+    """
+    async def normalize_key(self, request, controller_args, controller_kwargs):
         try:
             if self.path_in_key:
-                ret = "{}{}".format(controller_kwargs[self.param_name], request.path)
+                ret = "{}{}".format(
+                    controller_kwargs[self.param_name],
+                    request.path
+                )
+
             else:
                 ret = String(controller_kwargs[self.param_name])
+
         except KeyError:
             ret = ""
+
         return ret
 
     def definition(self, param_name, *args, **kwargs):
         self.param_name = param_name
-        return super(ratelimit_param, self).definition(*args, **kwargs)
+        return super().definition(*args, **kwargs)
 
 
 class ratelimit_param_ip(ratelimit_param):
-    """this is a combination of the limit_param and limit_ip decorators, it will allow
-    the param N times on the given unique ip"""
-    def normalize_key(self, request, controller_args, controller_kwargs):
+    """this is a combination of the limit_param and limit_ip decorators, it will
+    allow the param N times on the given unique ip
+    """
+    async def normalize_key(self, request, controller_args, controller_kwargs):
         try:
             if self.path_in_key:
-                ret = "{}.{}{}".format(controller_kwargs[self.param_name], request.ip, request.path)
+                ret = "{}.{}{}".format(
+                    controller_kwargs[self.param_name],
+                    request.ip,
+                    request.path
+                )
+
             else:
-                ret = "{}.{}".format(controller_kwargs[self.param_name], request.ip)
+                ret = "{}.{}".format(
+                    controller_kwargs[self.param_name],
+                    request.ip
+                )
+
         except KeyError:
             ret = ""
-        return ret
 
+        return ret
 
diff --git a/endpoints/decorators/utils.py b/endpoints/decorators/utils.py
index f034133..751f1fa 100644
--- a/endpoints/decorators/utils.py
+++ b/endpoints/decorators/utils.py
@@ -1,12 +1,9 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import types
 import re
 import cgi
-from functools import wraps
 import logging
 
-from decorators import FuncDecorator, property
+from .base import ControllerDecorator
 
 from ..compat import *
 from ..exception import CallError
@@ -15,7 +12,7 @@
 logger = logging.getLogger(__name__)
 
 
-class httpcache(FuncDecorator):
+class httpcache(ControllerDecorator):
     """
     sets the cache headers so the response can be cached by the client
 
@@ -23,44 +20,67 @@ class httpcache(FuncDecorator):
 
     ttl -- integer -- how many seconds to have the client cache the request
     """
-    def decorate(self, func, ttl):
-        def decorated(self, *args, **kwargs):
-            self.response.add_headers({
-                "Cache-Control": "max-age={}".format(ttl),
-            })
-            return func(self, *args, **kwargs)
-            # TODO -- figure out how to set ETag
-            #if not self.response.has_header('ETag')
+    def definition(self, ttl, **kwargs):
+        self.ttl = int(ttl)
+        super().definition(**kwargs)
 
-        return decorated
+    async def handle(self, controller, *args, **kwargs):
+        controller.response.add_headers({
+            "Cache-Control": "max-age={}".format(self.ttl),
+        })
+        # TODO -- figure out how to set ETag
+        #if not self.response.has_header('ETag')
 
 
-class nohttpcache(FuncDecorator):
+class nohttpcache(ControllerDecorator):
     """
     sets all the no cache headers so the response won't be cached by the client
 
     https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers#cache-prevention
     """
-    def decorate(self, func):
-        def decorated(self, *args, **kwargs):
-            self.response.add_headers({
-                "Cache-Control": "no-cache, no-store, must-revalidate",
-                "Pragma": "no-cache", 
-                "Expires": "0"
-            })
-            return func(self, *args, **kwargs)
+    async def handle(self, controller, *args, **kwargs):
+        controller.response.add_headers({
+            "Cache-Control": "no-cache, no-store, must-revalidate",
+            "Pragma": "no-cache", 
+            "Expires": "0"
+        })
 
-        return decorated
 
+class code_error(ControllerDecorator):
+    """
+    When placed on HTTPMETHOD methods (eg, GET) this will allow you to easily
+    map raised exceptions to http status codes
+
+    :example:
+        class Foo(Controller):
+            @code_error(406, AttributeError, IndexError)
+            def GET(self): raise AttributeError()
+
+    :param code: integer, an http status code
+        https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+    :param **exc_classes: tuple, one or more exception classes that will be
+        checked against the raised error
+    """
+    def definition(self, code, *exc_classes):
+        self.code = code
+        self.exc_classes = exc_classes
 
-class param(FuncDecorator):
+    async def handle_controller(self, *args, **kwargs):
+        try:
+            return await super().handle_controller(*args, **kwargs)
+
+        except self.exc_classes as e:
+            raise CallError(self.code, e) from e
+
+
+class param(ControllerDecorator):
     """
     decorator to allow setting certain expected query/body values and options
 
     this tries to be as similar to python's built-in argparse as possible
 
-    This checks both POST and GET query args, if you would like to only check POST,
-    use post_param, if you only want to check GET, then use get_param
+    This checks both POST and GET query args, if you would like to only check
+    POST, use post_param, if you only want to check GET, then use get_param
 
     example --
 
@@ -72,30 +92,51 @@ class param(FuncDecorator):
         type -- type -- a python type like int or float
         action -- string --
             store -- default
-            store_false -- set if you want default to be true, and false if param is
-                passed in
+            store_false -- set if you want default to be true, and false if
+                param is passed in
             store_true -- opposite of store_false
-            store_list -- set to have a value like 1,2,3 be blown up to ['1', '2', '3']
+            store_list -- set to have a value like 1,2,3 be blown up to
+                ['1', '2', '3']
             append -- if multiple param values should be turned into an array
                 eg, foo=1&foo=2 would become foo=[1, 2]
-            append_list -- it's store_list + append, so foo=1&foo=2,3 would be foo=[1, 2, 3]
-        default -- mixed -- the value that should be set if query param isn't there, if this is
-            callable (eg, time.time or datetime.utcnow) then it will be called every time the
-            decorated method is called
+            append_list -- it's store_list + append, so foo=1&foo=2,3 would be
+                foo=[1, 2, 3]
+        default -- mixed -- the value that should be set if query param isn't
+            there, if this is callable (eg, time.time or datetime.utcnow) then
+            it will be called every time the decorated method is called
         required -- boolean -- True if param is required, default is true
-        choices -- set() -- a set of values to be in tested against (eg, val in choices)
+        choices -- set() -- a set of values to be in tested against (eg, val in
+            choices)
         allow_empty -- boolean -- True allows values like False, 0, '' through,
-            default False, this will also let through any empty value that was set
-            via the default flag
+            default False, this will also let through any empty value that was
+            set via the default flag
         max_size -- int -- the maximum size of the param
         min_size -- int -- the minimum size of the param
-        regex -- regexObject -- if you would like the param to be validated with a regular
-            exception, uses the re.search() method
+        regex -- regexObject -- if you would like the param to be validated with
+            a regular exception, uses the re.search() method
         help -- string -- a helpful description for this param
 
     raises -- 
         CallError -- with 400 status code on any param validation failures
     """
+    def definition(self, *names, **flags):
+        self.normalize_type(names)
+        self.normalize_flags(flags)
+
+    async def handle_controller(self, func, controller, controller_args, controller_kwargs):
+        controller, cargs, ckwargs = self.normalize_param(
+            controller,
+            controller_args,
+            controller_kwargs,
+        )
+
+        return await super().handle_controller(
+            func,
+            controller,
+            cargs,
+            ckwargs,
+        )
+
     def normalize_flags(self, flags):
         """normalize the flags to make sure needed values are there
 
@@ -120,15 +161,16 @@ def normalize_flags(self, flags):
         self.flags = flags
 
     def normalize_type(self, names):
-        """Decide if this param is an arg or a kwarg and set appropriate internal flags"""
+        """Decide if this param is an arg or a kwarg and set appropriate
+        internal flags"""
         self.name = names[0]
         self.is_kwarg = False
         self.is_arg = False
         self.names = []
 
         try:
-            # http://stackoverflow.com/a/16488383/5006 uses ask forgiveness because
-            # of py2/3 differences of integer check
+            # http://stackoverflow.com/a/16488383/5006 uses ask forgiveness
+            # because of py2/3 differences of integer check
             self.index = int(self.name)
             self.name = ""
             self.is_arg = True
@@ -152,8 +194,8 @@ def normalize_default(self, default):
         return ret
 
     def normalize_param(self, slf, args, kwargs):
-        """this is where all the magic happens, this will try and find the param and
-        put its value in kwargs if it has a default and stuff"""
+        """this is where all the magic happens, this will try and find the
+        param and put its value in kwargs if it has a default and stuff"""
         if self.is_kwarg:
             kwargs = self.normalize_kwarg(slf.request, kwargs)
         else:
@@ -167,7 +209,9 @@ def normalize_arg(self, request, args):
 
         paction = flags['action']
         if paction not in set(['store', 'store_false', 'store_true']):
-            raise RuntimeError('unsupported positional param action {}'.format(paction))
+            raise RuntimeError('unsupported positional param action {}'.format(
+                paction
+            ))
 
         if 'dest' in flags:
             logger.warn("dest is ignored in positional param")
@@ -177,7 +221,10 @@ def normalize_arg(self, request, args):
 
         except IndexError:
             if flags["required"]:
-                raise CallError(400, "required positional param at index {} does not exist".format(index))
+                raise CallError(
+                    400,
+                    f"required positional param at index {index} does not exist"
+                )
 
             else:
                 val = self.normalize_default(flags.get('default', None))
@@ -186,7 +233,10 @@ def normalize_arg(self, request, args):
             val = self.normalize_val(request, val)
 
         except ValueError as e:
-            raise CallError(400, "Positional arg {} failed with {}".format(index, String(e)))
+            raise CallError(
+                400,
+                "Positional arg {} failed with {}".format(index, String(e))
+            )
 
         args.insert(index, val)
 
@@ -200,7 +250,8 @@ def find_kwarg(self, request, names, required, default, kwargs):
         :param required: True if a name has to be found in kwargs
         :param default: the default value if name isn't found
         :param kwargs: the kwargs that will be used to find the value
-        :returns: tuple, found_name, val where found_name is the actual name kwargs contained
+        :returns: tuple, found_name, val where found_name is the actual name
+            kwargs contained
         """
         val = default
         found_name = ''
@@ -211,7 +262,9 @@ def find_kwarg(self, request, names, required, default, kwargs):
                 break
 
         if not found_name and required:
-            raise ValueError("required param {} does not exist".format(self.name))
+            raise ValueError("required param {} does not exist".format(
+                self.name
+            ))
 
         return found_name, val
 
@@ -225,13 +278,20 @@ def normalize_kwarg(self, request, kwargs):
             dest_name = flags.get('dest', name)
 
             has_val = True
-            found_name, val = self.find_kwarg(request, self.names, prequired, pdefault, kwargs)
+            found_name, val = self.find_kwarg(
+                request,
+                self.names,
+                prequired,
+                pdefault,
+                kwargs
+            )
             if found_name:
                 # we are going to replace found_name with dest_name
                 kwargs.pop(found_name)
             else:
-                # we still want to run a default value through normalization but if we
-                # didn't find a value and don't have a default, don't set any value
+                # we still want to run a default value through normalization but
+                # if we didn't find a value and don't have a default, don't set
+                # any value
                 has_val = 'default' in flags
 
             if has_val:
@@ -329,14 +389,25 @@ def normalize_val(self, request, val):
             if isinstance(val, list) and ptype != list:
                 for v in val:
                     if v not in pchoices:
-                        raise ValueError("param value {} not in choices {}".format(v, pchoices))
+                        raise ValueError(
+                            "param value {} not in choices {}".format(
+                                v,
+                                pchoices
+                            )
+                        )
 
             else:
                 if val not in pchoices:
-                    raise ValueError("param value {} not in choices {}".format(val, pchoices))
+                    raise ValueError(
+                        "param value {} not in choices {}".format(
+                            val,
+                            pchoices
+                        )
+                    )
 
         # at some point this if statement is just going to be too ridiculous
-        # FieldStorage check is because of this bug https://bugs.python.org/issue19097
+        # FieldStorage check is because of this bug
+        # https://bugs.python.org/issue19097
         if not isinstance(val, cgi.FieldStorage):
             if not allow_empty and val is not False and not val:
                 if 'default' not in flags:
@@ -364,21 +435,12 @@ def normalize_val(self, request, val):
 
         return val
 
-    def decorate(slf, func, *names, **flags):
-        slf.normalize_type(names)
-        slf.normalize_flags(flags)
-
-        def decorated(self, *args, **kwargs):
-            self, args, kwargs = slf.normalize_param(self, args, kwargs)
-            return func(self, *args, **kwargs)
-        return decorated
-
 
 class param_query(param):
     """same as param, but only checks GET params"""
     def find_kwarg(self, request, names, required, default, kwargs):
         try:
-            return super(param_query, self).find_kwarg(
+            return super().find_kwarg(
                 request,
                 names,
                 required,
@@ -386,8 +448,12 @@ def find_kwarg(self, request, names, required, default, kwargs):
                 request.query_kwargs
             )
 
-        except ValueError:
-            raise ValueError("required param {} was not present in GET params".format(self.name))
+        except ValueError as e:
+            raise ValueError(
+                "required param {} was not present in GET params".format(
+                    self.name
+                )
+            ) from e
 
 
 class param_body(param):
@@ -402,33 +468,10 @@ def find_kwarg(self, request, names, required, default, kwargs):
                 request.body_kwargs
             )
 
-        except ValueError:
-            raise ValueError("required param {} was not present in POST params".format(self.name))
-
-
-class code_error(FuncDecorator):
-    """
-    When placed on HTTPMETHOD methods (eg, GET) this will allow you to easily map
-    raised exceptions to http status codes
-
-    :example:
-        class Foo(Controller):
-            @code_error(406, AttributeError, IndexError)
-            def GET(self): raise AttributeError()
-
-    :param code: integer, an http status code
-        https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
-    :param **exc_classes: tuple, one or more exception classes that will be checked
-        against the raised error
-    """
-    def decorate(self, func, code, *exc_classes):
-        def decorated(self, *args, **kwargs):
-            try:
-                return func(self, *args, **kwargs)
-
-            except exc_classes as e:
-                raise CallError(code, e)
-
-        return decorated
-
+        except ValueError as e:
+            raise ValueError(
+                "required param {} was not present in POST params".format(
+                    self.name
+                )
+            ) from e
 
diff --git a/endpoints/environ.py b/endpoints/environ.py
deleted file mode 100644
index b385e2a..0000000
--- a/endpoints/environ.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-
-from datatypes.environ import Environ
-
-
-environ = Environ(__name__)
-get = environ.get
-paths = environ.paths
-
-
-ENCODING = get("ENCODING", "UTF-8")
-"""Default encoding"""
-
-
-RESPONSE_CONTENT_TYPE = get("RESPONSE_CONTENT_TYPE", "application/json")
-"""the response content type a controller will set"""
-
-
-HOST = get("HOST", "")
-"""The host string, usually just domain or domain:port, this is used by the server
-classes and also the tests"""
-
-
-def set_host(host):
-    global HOST
-    HOST = host
-    environ.set("HOST", host)
-
-
-def set_controller_prefixes(prefixes, env_name='ENDPOINTS_PREFIX'):
-    """set the controller_prefixes found in env_name to prefixes, this will remove
-    any existing found controller prefixes 
-
-    :param prefixes: list, the new prefixes that will replace any old prefixes
-    :param env_name: string, the name of the environment variables
-    """
-    environ.nset(env_name, prefixes)
-
-
-def get_prefix_names(env_name):
-    """This returns the actual environment variable names from * -> *_N
-
-    :param env_name: string, the name of the environment variables
-    :returns: generator, the found environment names
-    """
-    for k in environ.nkeys(env_name):
-        yield k
-
-
-def get_controller_prefixes(env_name='ENDPOINTS_PREFIX'):
-    """this will look for ENDPOINTS_PREFIX, and ENDPOINTS_PREFIX_N (where
-    N is 1 to infinity) in the environment, if it finds them, it will assume they
-    are python module paths where endpoints can find Controller subclasses
-
-    The num checks (eg ENDPOINTS_PREFIX_1, ENDPOINTS_PREFIX_2) go in order, so you
-    can't do ENDPOINTS_PREFIX_1, ENDPOINTS_PREFIX_3, because it will fail on _2
-    and move on, so make sure your num dsns are in order (eg, 1, 2, 3, ...)
-
-    :Example:
-        export ENDPOINTS_PREFIX_1=foo.controllers
-        export ENDPOINTS_PREFIX_2=bar.che
-        $ python
-        >>> from endpoints import environ
-        >>> environ.get_controller_prefixes
-        ['foo.controller', 'bar.che']
-
-    :param env_name: string, the name of the environment variables
-    :returns: list, the found module paths
-    """
-    return list(environ.paths(env_name))
-
diff --git a/endpoints/exception.py b/endpoints/exception.py
index d9cd3c0..452cab3 100644
--- a/endpoints/exception.py
+++ b/endpoints/exception.py
@@ -19,6 +19,11 @@ def __init__(self, code=500, msg='', *args, **kwargs):
         self.headers = kwargs.pop('headers', {})
         super(CallError, self).__init__(msg, *args, **kwargs)
 
+    def __str__(self):
+        if not (s := super().__str__()):
+            s = f"{self.__class__.__name__}({self.code})"
+        return s
+
 
 class Redirect(CallError):
     """controllers can raise this to redirect to the new location"""
@@ -27,7 +32,7 @@ def __init__(self, location, code=302, **kwargs):
         headers = kwargs.pop('headers', {})
         headers.setdefault('Location', location)
         kwargs['headers'] = headers
-        super(Redirect, self).__init__(code, location, **kwargs)
+        super().__init__(code, location, **kwargs)
 
 
 class AccessDenied(CallError):
@@ -68,7 +73,7 @@ def __init__(self, msg='', scheme="", realm="", **kwargs):
         v = "{} realm=\"{}\"".format(self.scheme, self.realm) if self.realm else self.scheme
         kwargs["headers"].setdefault("WWW-Authenticate", v)
 
-        super(AccessDenied, self).__init__(401, msg, **kwargs)
+        super().__init__(401, msg, **kwargs)
 
 
 class CallStop(CallError):
@@ -84,14 +89,14 @@ def __init__(self, code, body=None, msg='', **kwargs):
         body -- mixed -- the body of the response
         '''
         self.body = body
-        super(CallStop, self).__init__(code, msg, **kwargs)
+        super().__init__(code, msg, **kwargs)
 
 
 class RouteError(CallError):
     """Raised when @route fails on a Controller method"""
     def __init__(self, instance, code=-1, msg=""):
         self.instance = instance
-        super(RouteError, self).__init__(code=code, msg=msg)
+        super().__init__(code=code, msg=msg)
 
 
 class VersionError(RouteError):
@@ -99,7 +104,7 @@ class VersionError(RouteError):
     def __init__(self, instance, request_version, versions):
         self.request_version = request_version
         self.versions = versions
-        super(VersionError, self).__init__(instance, code=-2)
+        super().__init__(instance, code=-2)
 
 
 class CloseConnection(CallError):
@@ -113,5 +118,5 @@ def __init__(self, msg='', *args, **kwargs):
 
         :param msg: string, the message you want to accompany your close
         '''
-        super(CloseConnection, self).__init__(0, msg, *args, **kwargs)
+        super().__init__(0, msg, *args, **kwargs)
 
diff --git a/endpoints/extras/__init__.py b/endpoints/extras/__init__.py
deleted file mode 100644
index c60116d..0000000
--- a/endpoints/extras/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-
diff --git a/endpoints/extras/html/README.md b/endpoints/extras/html/README.md
deleted file mode 100644
index 0081364..0000000
--- a/endpoints/extras/html/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Endpoints HTML Support
-
-This uses [Jinja2](https://jinja.palletsprojects.com/) for HTML templating.
-
-You can install all the dependencies for this module using pip:
-
-```bash
-$ pip install endpoints[html]
-```
-
-## Template Folder
-
-You can place all your template files (which should end with `.html`) in a directory and then add an environment variable so Endpoints can pick it up:
-
-```bash
-export ENDPOINTS_HTML_TEMPLATE_PATH="/your/path/to/html/template/files"
-```
-
-## View Decorator
-
-Once you have your templates created, you can access them using the `view` decorator on a Controller:
-
-```python
-from endpoints import Controller
-from endpoints.extras.html import view
-
-
-class Default(Controller):
-    @view("<TEMPLATE_NAME>")
-    def GET(self):
-        return {
-            "foo": "bar"
-        }
-```
-
-The values returned from the `GET` method will be passed to the `<TEMPLATE_NAME>` template and the rendered html will be returned to the client.
\ No newline at end of file
diff --git a/endpoints/extras/html/__init__.py b/endpoints/extras/html/__init__.py
deleted file mode 100644
index 6179432..0000000
--- a/endpoints/extras/html/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from .core import Templates
-from .decorators import view
-
diff --git a/endpoints/extras/html/core.py b/endpoints/extras/html/core.py
deleted file mode 100644
index 87426d5..0000000
--- a/endpoints/extras/html/core.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from jinja2 import Environment, FileSystemLoader
-
-from ...compat import *
-from ... import environ
-from ...utils import Path, String
-
-
-class Templates(object):
-    """Handles Jinja2 template rendering
-
-    Based on code originally from bang.config.Theme
-
-    Template functionality is a thin wrapper around Jinja functionality that
-    handles templating things
-
-    Jinja docs:
-        http://jinja.pocoo.org/docs/dev/
-        https://jinja.palletsprojects.com/en/master/api/
-
-    Jinja template syntax documentation:
-        https://jinja.palletsprojects.com/en/master/templates/
-    """
-    def __init__(self, directories=None, **kwargs):
-
-        if not directories:
-            directories = list(environ.paths("HTML_TEMPLATE_PATH")) + list(environ.paths("HTML_TEMPLATE_DIR"))
-        self.directories = [Path(d) for d in directories]
-
-        # https://jinja.palletsprojects.com/en/master/api/#jinja2.Environment
-        self.interface = Environment(
-            loader=FileSystemLoader(self.directories),
-            #extensions=['jinja2.ext.with_'] # http://jinja.pocoo.org/docs/dev/templates/#with-statement
-            lstrip_blocks=kwargs.pop("lstrip_blocks", True),
-            trim_blocks=kwargs.pop("trim_blocks", True),
-        )
-
-        self.templates = {}
-        for d in self.directories:
-            for f in d.rglob("*.html"):
-                rel_f = f.relative_to(d)
-                #fileroot, fileext = os.path.splitext(rel_f)
-                self.templates[rel_f] = f
-
-    def get_template_name(self, template_name):
-        if not template_name.endswith(".html"):
-            template_name += ".html"
-        return template_name
-
-    def render(self, template_name, d=None, **kwargs):
-        """
-        https://jinja.palletsprojects.com/en/master/api/#jinja2.Template.render
-        """
-        d = d or {}
-        d.update(kwargs)
-
-        tmpl = self.interface.get_template(self.get_template_name(template_name))
-        html = tmpl.render(**d)
-        return html
-
-    def has(self, template_name):
-        """Return True if template_name exists"""
-        return self.get_template_name(template_name) in self.templates
-
diff --git a/endpoints/extras/html/decorators.py b/endpoints/extras/html/decorators.py
deleted file mode 100644
index e7507ec..0000000
--- a/endpoints/extras/html/decorators.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from ...compat import *
-from ...decorators import FuncDecorator
-from ...utils import MimeType, String
-from .core import Templates
-
-
-class view(FuncDecorator):
-    """take the controller's returned value and template it using the passed in
-    template_name, which does not need an extension
-
-    :Example:
-        class Default(Controller):
-            @html("foo")
-            def GET(self, **kwargs):
-                return kwargs
-    """
-    template_class = Templates
-
-    def decorate(self, f, template_name, **kwargs):
-
-        template_class = kwargs.pop("template_class", self.template_class)
-        directories = kwargs.pop("directories", None)
-        renderer = template_class(directories)
-        content_type = MimeType.find("html")
-
-        def decorated(self, *args, **kwargs):
-            request = self.request
-
-            response = self.response
-            response.set_header("Content-Type", content_type)
-
-            # let's convert the controller's return value into actual html
-            d = f(self, *args, **kwargs)
-
-            d.setdefault("template_name", template_name)
-            d.setdefault("controller", self)
-            d.setdefault("request", request)
-            d.setdefault("response", response)
-
-            return renderer.render(template_name, d)
-
-        return decorated
-
diff --git a/endpoints/http.py b/endpoints/http.py
deleted file mode 100644
index 4b13760..0000000
--- a/endpoints/http.py
+++ /dev/null
@@ -1,858 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-import json
-import types
-import re
-from functools import partial
-import itertools
-import logging
-import inspect
-import copy
-from socket import gethostname
-import cgi
-import io
-
-from datatypes import (
-    Url as BaseUrl,
-    Host,
-    HTTPHeaders as BaseHeaders,
-    HTTPEnviron as Environ,
-)
-
-from .compat import *
-from .decorators.utils import property # must be .utils because circular dep
-from .utils import (
-    AcceptHeader,
-    MimeType,
-    Base64,
-    Deepcopy,
-    FileWrapper,
-)
-
-
-logger = logging.getLogger(__name__)
-
-
-class Headers(BaseHeaders):
-    def is_plain(self):
-        """return True if body's content-type is text/plain"""
-        ct = self.get("Content-Type", "")
-        return "plain" in ct
-
-    def is_json(self):
-        """return True if body's content-type is application/json"""
-        ct = self.get("Content-Type", "")
-        return "json" in ct
-
-    def is_urlencoded(self):
-        """return True if body's content-type is application/x-www-form-urlencoded"""
-        ct = self.get("Content-Type", "")
-        return ("form-urlencoded" in ct) or ("form-data" in ct)
-
-    def is_multipart(self):
-        """return True if body's content-type is multipart/form-data"""
-        ct = self.get("Content-Type", "")
-        return "multipart" in ct
-
-
-class Body(cgi.FieldStorage, object):
-    """Wraps the default FieldStorage to handle json and also recovers when the
-    input fails to parse correctly
-
-    https://github.com/python/cpython/blob/2.7/Lib/cgi.py
-    https://github.com/python/cpython/blob/3.8/Lib/cgi.py
-    """
-    FieldStorageClass = cgi.FieldStorage
-
-    @property(cached="_args")
-    def args(self):
-        return getattr(self, "json_args", [])
-
-    @property(cached="_kwargs")
-    def kwargs(self):
-        body_kwargs = {}
-        body_kwargs.update(getattr(self, "json_kwargs", {}))
-
-        # we only have a list when we had a multiport or data-form submission
-        if getattr(self, "list"):
-            for field_name in self.keys():
-                body_field = self[field_name]
-                if body_field.filename:
-                    body_kwargs[field_name] = FileWrapper(
-                        body_field.file,
-                        name=body_field.filename,
-                        filename=body_field.filename,
-                        type=body_field.type,
-                        raw=body_field,
-                    )
-
-                else:
-                    body_kwargs[field_name] = body_field.value
-
-
-        return body_kwargs
-
-    def __init__(self, fp, request, **kwargs):
-        if request.headers.get('transfer-encoding', "").lower().startswith("chunked"):
-            raise IOError("Chunked bodies are not supported")
-
-        self.request = request
-
-        # py3 compatibility
-        self.encoding = request.encoding
-        self.errors = "replace"
-        self.max_num_fields = None
-
-        self.name = self.filename = self.value = None
-        self.length = int(request.headers.get("CONTENT_LENGTH", -1))
-        self.fp = fp
-        self.list = None
-
-        if self.length > 0:
-            if request.is_json():
-                self.read_json()
-
-            else:
-                kwargs.setdefault("keep_blank_values", True)
-
-                # so FieldStorage parses the body in the constructor and if it fails
-                # then the body instance won't be created, so this is set to True and
-                # the error is handled in read_urlencoded
-                kwargs["strict_parsing"] = True
-
-                super(Body, self).__init__(
-                    fp=fp,
-                    headers=request.headers,
-                    environ=request.environ,
-                    **kwargs
-                )
-
-    def is_plain(self):
-        """return True if body's content-type is text/plain"""
-        return self.request.headers.is_plain()
-
-    def is_json(self):
-        """return True if body's content-type is application/json"""
-        return self.request.headers.is_json()
-
-    def is_urlencoded(self):
-        """return True if body's content-type is application/x-www-form-urlencoded"""
-        return self.request.headers.is_urlencoded()
-
-    def is_multipart(self):
-        """return True if body's content-type is multipart/form-data"""
-        return self.request.headers.is_multipart()
-
-    def read_json(self):
-        body = self.fp.read(self.length)
-        self.file = io.BytesIO(body)
-
-        body_args = []
-        body_kwargs = {}
-        b = json.loads(body)
-        if isinstance(b, list):
-            body_args = b
-
-        elif isinstance(b, dict):
-            body_kwargs = b
-
-        else:
-            body_args = [b]
-
-        self.json_args = body_args
-        self.json_kwargs = body_kwargs
-
-    def read_urlencoded(self):
-        """Internal: read data in query string format."""
-        body = self.fp.read(self.length)
-        self.file = io.BytesIO(body)
-
-        qs = String(body, self.encoding, self.errors)
-
-        if self.qs_on_post:
-            qs += '&' + self.qs_on_post
-
-        try:
-            if is_py2:
-                query = parse.parse_qsl(
-                    qs,
-                    self.keep_blank_values,
-                    self.strict_parsing,
-                )
-
-            else:
-                query = parse.parse_qsl(
-                    qs,
-                    self.keep_blank_values,
-                    self.strict_parsing,
-                    encoding=self.encoding,
-                    errors=self.errors,
-                    max_num_fields=self.max_num_fields
-                )
-
-        except ValueError:
-            # if the right headers were sent then this should error
-            if self.is_urlencoded() or self.is_multipart():
-                raise
-
-        else:
-            self.list = [cgi.MiniFieldStorage(key, value) for key, value in query]
-            self.skip_lines()
-
-    def make_file(self, *args, **kwargs):
-        return io.BytesIO()
-
-    def seek(self, *args, **kwargs):
-        return self.file.seek(*args, **kwargs)
-
-    def read(self, *args, **kwargs):
-        return self.file.read(*args, **kwargs)
-
-    def tell(self, *args, **kwargs):
-        return self.file.tell(*args, **kwargs)
-
-
-class Url(BaseUrl):
-    """a url object on steroids, this is here to make it easy to manipulate urls
-    we try to map the supported fields to their urlparse equivalents, with some additions
-
-    https://tools.ietf.org/html/rfc3986.html
-
-    given a url http://user:pass@foo.com:1000/bar/che?baz=boom#anchor
-    with a controller: Bar
-
-    .scheme = http
-    .netloc = user:pass@foo.com:1000
-    .hostloc = foo.com:1000
-    .hostname = foo.com
-    .host() = http://foo.com
-    .port = 1000
-    .base = http://user:pass@foo.com:1000/bar/che
-    .fragment = anchor
-    .anchor = fragment
-    .uri = /bar/che?baz=boom#anchor
-    .host(...) = http://foo.com/...
-    .base(...) = http://foo.com/bar/che/...
-    .controller(...) = http://foo.com/bar/...
-    """
-    class_path = ""
-
-    module_path = ""
-
-    def module(self, *paths, **query_kwargs):
-        """create a new Url instance using the module path as a base
-
-        :param *paths: list, the paths to append to the module path
-        :param **query_kwargs: dict, any query string params to add
-        :returns: new Url instance
-        """
-        kwargs = self._normalize_params(*paths, **query_kwargs)
-        if self.module_path:
-            if "path" in kwargs:
-                paths = self.normalize_paths(self.module_path, kwargs["path"])
-                kwargs["path"] = "/".join(paths)
-            else:
-                kwargs["path"] = self.module_path
-        return self.create(self.root, **kwargs)
-
-    def controller(self, *paths, **query_kwargs):
-        """create a new url object using the controller path as a base
-
-        if you have a controller `foo.BarController` then this would create a new
-        Url instance with `host/foo/bar` as the base path, so any *paths will be
-        appended to `/foo/bar`
-
-        :example:
-            # controller foo.Bar(Controller)
-
-            print url # http://host.com/foo/bar/some_random_path
-
-            print url.controller() # http://host.com/foo/bar
-            print url.controller("che", boom="bam") # http://host/foo/bar/che?boom=bam
-
-        :param *paths: list, the paths to append to the controller path
-        :param **query_kwargs: dict, any query string params to add
-        :returns: new Url instance
-        """
-        kwargs = self._normalize_params(*paths, **query_kwargs)
-        if self.class_path:
-            if "path" in kwargs:
-                paths = self.normalize_paths(self.class_path, kwargs["path"])
-                kwargs["path"] = "/".join(paths)
-            else:
-                kwargs["path"] = self.class_path
-        return self.create(self.root, **kwargs)
-
-
-class Http(object):
-    header_class = Headers
-
-    def __init__(self):
-        self.headers = Headers()
-
-    def has_header(self, header_name):
-        """return true if the header is set"""
-        return header_name in self.headers
-
-    def set_headers(self, headers):
-        """replace all headers with passed in headers"""
-        self.headers = Headers(headers)
-
-    def add_headers(self, headers, **kwargs):
-        self.headers.update(headers, **kwargs)
-
-    def set_header(self, header_name, val):
-        self.headers[header_name] = val
-
-    def add_header(self, header_name, val, **params):
-        self.headers.add_header(header_name, val, **params)
-
-    def get_header(self, header_name, default_val=None):
-        """try as hard as possible to get a a response header of header_name,
-        rreturn default_val if it can't be found"""
-        return self.headers.get(header_name, default_val)
-
-    def find_header(self, header_names, default_val=None):
-        """given a list of headers return the first one you can, default_val if you
-        don't find any
-
-        :param header_names: list, a list of headers, first one found is returned
-        :param default_val: mixed, returned if no matching header is found
-        :returns: mixed, the value of the header or default_val
-        """
-        ret = default_val
-        for header_name in header_names:
-            if self.has_header(header_name):
-                ret = self.get_header(header_name, default_val)
-                break
-        return ret
-
-    def _parse_query_str(self, query):
-        """return name=val&name2=val2 strings into {name: val} dict"""
-        u = Url(query=query)
-        return u.query_kwargs
-
-    def _build_body_str(self, b):
-        # we are returning the body, let's try and be smart about it and match content type
-        ct = self.get_header('content-type')
-        if ct:
-            ct = ct.lower()
-            if ct.rfind("json") >= 0:
-                if b:
-                    b = json.dumps(b)
-                else:
-                    b = None
-
-            elif ct.rfind("x-www-form-urlencoded") >= 0:
-                b = urlencode(b, doseq=True)
-
-        return b
-
-    def copy(self):
-        """nice handy wrapper around the deepcopy"""
-        return copy.deepcopy(self)
-
-    def __deepcopy__(self, memodict=None):
-        memodict = memodict or {}
-
-        memodict.setdefault("controller_info", getattr(self, "controller_info", {}))
-        memodict.setdefault("body", getattr(self, "body", None))
-
-        return Deepcopy(ignore_private=True).copy(self, memodict)
-
-    def is_json(self):
-        return self.headers.is_json()
-
-
-class Request(Http):
-    '''
-    common interface that endpoints uses to decide what to do with the incoming request
-
-    an instance of this class is used by the endpoints Call instance to decide where endpoints
-    should route requests, so, many times, you'll need to write a glue function that takes however
-    your request data is passed to Python and convert it into a Request instance that endpoints can
-    understand
-
-    properties --
-
-    headers -- a dict of all the request headers in { header_name: header_val } format
-    path -- the /path/part/of/the/url
-    path_args -- tied to path, it's path, but divided by / so all the path bits are returned as a list
-    query -- the ?name=val portion of a url
-    query_kwargs -- tied to query, the values in query but converted to a dict {name: val}
-    '''
-    environ = None
-    """holds all the values that aren't considered headers but usually get passed with the request"""
-
-    raw_request = None
-    """the original raw request that was filtered through one of the interfaces"""
-
-    method = None
-    """the http method (GET, POST)"""
-
-    controller_info = None
-    """will hold the controller information for the request, populated from the Call"""
-
-    body_class = Body
-    """see create_body()"""
-
-    @property
-    def accept_content_type(self):
-        """Return the requested content type
-
-        https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
-
-        :returns: string, empty if a suitable content type wasn't found, this will
-            only check the first accept content type and then only if that content
-            type has no wildcards
-        """
-        v = ""
-        accept_header = self.get_header('accept', "")
-        if accept_header:
-            a = AcceptHeader(accept_header)
-            for mt in a:
-                # we only care about the first value, and only if it has no wildcards
-                if "*" not in mt[0]:
-                    v = "/".join(mt[0])
-                break
-
-        return v
-
-    @property
-    def accept_encoding(self):
-        """The encoding the client requested the response to use"""
-        # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
-        ret = ""
-        accept_encoding = self.get_header("Accept-Charset", "")
-        if accept_encoding:
-            bits = re.split(r"\s+", accept_encoding)
-            bits = bits[0].split(";")
-            ret = bits[0]
-        return ret
-
-    @property(cached="_encoding")
-    def encoding(self):
-        """the character encoding of the request, usually only set in POST type requests"""
-        encoding = None
-        ct = self.get_header('content-type')
-        if ct:
-            ah = AcceptHeader(ct)
-            if ah.media_types:
-                encoding = ah.media_types[0][2].get("charset", None)
-
-        return encoding
-
-    @property
-    def access_token(self):
-        """return an Oauth 2.0 Bearer access token if it can be found"""
-        access_token = self.get_auth_bearer()
-        if not access_token:
-            access_token = self.query_kwargs.get('access_token', '')
-            if not access_token:
-                access_token = self.body_kwargs.get('access_token', '')
-
-        return access_token
-
-    @property
-    def client_tokens(self):
-        """try and get Oauth 2.0 client id and secret first from basic auth header,
-        then from GET or POST parameters
-
-        return -- tuple -- client_id, client_secret
-        """
-        client_id, client_secret = self.get_auth_basic()
-
-        if not client_id:
-            client_id = self.query_kwargs.get('client_id', '')
-            if not client_id:
-                client_id = self.body_kwargs.get('client_id', '')
-
-        if not client_secret:
-            client_secret = self.query_kwargs.get('client_secret', '')
-            if not client_secret:
-                client_secret = self.body_kwargs.get('client_secret', '')
-
-        return client_id, client_secret
-
-    @property(read_only="_ips")
-    def ips(self):
-        """return all the possible ips of this request, this will include public and private ips"""
-        r = []
-        names = ['X_FORWARDED_FOR', 'CLIENT_IP', 'X_REAL_IP', 'X_FORWARDED', 
-               'X_CLUSTER_CLIENT_IP', 'FORWARDED_FOR', 'FORWARDED', 'VIA',
-               'REMOTE_ADDR']
-
-        for name in names:
-            vs = self.get_header(name, '')
-            if vs:
-                r.extend(map(lambda v: v.strip(), vs.split(',')))
-
-            vs = self.environ.get(name, '')
-            if vs:
-                r.extend(map(lambda v: v.strip(), vs.split(',')))
-
-        return r
-
-    @property(read_only="_ip")
-    def ip(self):
-        """return the public ip address"""
-        r = ''
-
-        # this was compiled from here:
-        # https://github.com/un33k/django-ipware
-        # http://www.ietf.org/rfc/rfc3330.txt (IPv4)
-        # http://www.ietf.org/rfc/rfc5156.txt (IPv6)
-        # https://en.wikipedia.org/wiki/Reserved_IP_addresses
-        format_regex = re.compile(r'\s')
-        ip_regex = re.compile(r'^(?:{})'.format(r'|'.join([
-            r'0\.', # reserved for 'self-identification'
-            r'10\.', # class A
-            r'169\.254', # link local block
-            r'172\.(?:1[6-9]|2[0-9]|3[0-1])\.', # class B
-            r'192\.0\.2\.', # documentation/examples
-            r'192\.168', # class C
-            r'255\.{3}', # broadcast address
-            r'2001\:db8', # documentation/examples
-            r'fc00\:', # private
-            r'fe80\:', # link local unicast
-            r'ff00\:', # multicast
-            r'127\.', # localhost
-            r'\:\:1' # localhost
-        ])))
-
-        ips = self.ips
-        for ip in ips:
-            if not format_regex.search(ip) and not ip_regex.match(ip):
-                r = ip
-                break
-
-        return r
-
-    @property(cached="_host")
-    def host(self):
-        """return the request host"""
-        return self.get_header("host")
-
-    @property(cached="_scheme")
-    def scheme(self):
-        """return the request scheme (eg, http, https)"""
-        scheme = self.environ.get('wsgi.url_scheme', "http")
-        return scheme
-
-    @property(cached="_port")
-    def port(self):
-        """return the server port"""
-        return int(self.environ.get('SERVER_PORT', 0))
-
-    @property
-    def host_url(self):
-        """return the request host as a Url instance"""
-        return self.url.host_url()
-
-    @property
-    def url(self):
-        """return the full request url as an Url() instance"""
-        scheme = self.scheme
-        host = self.host
-        path = self.path
-        query = self.query
-        port = self.port
-
-        # normalize the port
-        host_domain, host_port = Url.split_hostname_from_port(host)
-        if host_port:
-            port = host_port
-
-        class_path = ""
-        module_path = ""
-        if self.controller_info:
-            class_path = self.controller_info.get("class_path", "")
-            module_path = self.controller_info.get("module_path", "")
-
-        u = Url(
-            scheme=scheme,
-            hostname=host,
-            path=path,
-            query=query,
-            port=port,
-            class_path=class_path,
-            module_path=module_path
-        )
-        return u
-
-    @property(cached="_path")
-    def path(self):
-        """path part of a url (eg, http://host.com/path?query=string)"""
-        self._path = ''
-        path_args = self.path_args
-        path = "/{}".format("/".join(path_args))
-        return path
-
-    @property(cached="_path_args")
-    def path_args(self):
-        """the path converted to list (eg /foo/bar becomes [foo, bar])"""
-        self._path_args = []
-        path = self.path
-        path_args = list(filter(None, path.split('/')))
-        return path_args
-
-    @property(cached="_query")
-    def query(self):
-        """query_string part of a url (eg, http://host.com/path?query=string)"""
-        self._query = query = ""
-
-        query_kwargs = self.query_kwargs
-        if query_kwargs: query = urlencode(query_kwargs, doseq=True)
-        return query
-
-    @property(cached="_query_kwargs")
-    def query_kwargs(self):
-        """{foo: bar, baz: che}"""
-        self._query_kwargs = query_kwargs = {}
-        query = self.query
-        if query: query_kwargs = self._parse_query_str(query)
-        return query_kwargs
-
-    @property
-    def kwargs(self):
-        """combine GET and POST params to be passed to the controller"""
-        kwargs = dict(self.query_kwargs)
-        kwargs.update(self.body_kwargs)
-        return kwargs
-
-    def __init__(self):
-        self.environ = Environ()
-        self.body = None
-        self.body_args = []
-        self.body_kwargs = {}
-        super(Request, self).__init__()
-
-    def create_body(self, body):
-        return self.body_class(body, self)
-
-    def version(self, content_type="*/*"):
-        """by default, versioning is based off of this post 
-        http://urthen.github.io/2013/05/09/ways-to-version-your-api/
-        https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
-
-        This can be extended to implement versioning in any other way though, this
-        is used with the @version decorator
-
-        :param content_type: string, the content type you want to check version info,
-            by default this checks all content types, which is probably what most
-            want, since if you're doing accept header versioning you're probably
-            only passing up one content type
-        :returns: string, the found version
-        """
-        v = ""
-        accept_header = self.get_header('accept', "")
-        if accept_header:
-            a = AcceptHeader(accept_header)
-            for mt in a.filter(content_type):
-                v = mt[2].get("version", "")
-                if v: break
-
-        return v
-
-    def is_method(self, method):
-        """return True if the request method matches the passed in method"""
-        return self.method.upper() == method.upper()
-
-    def has_body(self):
-        #return self.method.upper() in set(['POST', 'PUT'])
-        return True if (self.body_kwargs or self.body_args) else False
-        #return True if self.body_kwargs else False
-        #return self.method.upper() not in set(['GET'])
-
-    def get_auth_bearer(self):
-        """return the bearer token in the authorization header if it exists"""
-        access_token = ''
-        auth_header = self.get_header('authorization')
-        if auth_header:
-            m = re.search(r"^Bearer\s+(\S+)$", auth_header, re.I)
-            if m: access_token = m.group(1)
-
-        return access_token
-
-    def get_auth_basic(self):
-        """return the username and password of a basic auth header if it exists"""
-        username = ''
-        password = ''
-        auth_header = self.get_header('authorization')
-        if auth_header:
-            m = re.search(r"^Basic\s+(\S+)$", auth_header, re.I)
-            if m:
-                auth_str = Base64.decode(m.group(1))
-                username, password = auth_str.split(':', 1)
-
-        return username, password
-
-    def get_auth_scheme(self):
-        """The authorization header is defined like:
-
-            Authorization = credentials
-            credentials = auth-scheme TOKEN_VALUE
-            auth-scheme = token
-
-        which roughly translates to:
-
-            Authorization: token TOKEN_VALUE
-
-        This returns the token part of the auth header's value
-
-        :returns: string, the authentication scheme (eg, Bearer, Basic)
-        """
-        scheme = ""
-        auth_header = self.get_header('authorization')
-        if auth_header:
-            m = re.search(r"^(\S+)\s+", auth_header)
-            if m:
-                scheme = m.group(1)
-        return scheme
-
-    def is_auth(self, scheme):
-        """Return True if scheme matches the authorization scheme
-
-        :Example:
-            # Authorization: Basic FOOBAR
-            request.is_auth("basic") # True
-            request.is_auth("bearer") # False
-        """
-        return scheme.lower() == self.get_auth_scheme().lower()
-
-    def is_oauth(self, scheme):
-        """Similar to .is_auth() but checks for a wider range of names and also
-        will check for values like "client_id" and "client_secret" being passed up
-        in the body because javascript doesn't want to set headers in websocket
-        connections
-
-        :param scheme: string, the scheme you want to check, usually "basic" or "bearer"
-        :return: boolean
-        """
-        scheme = scheme.lower()
-        if scheme in set(["bearer", "token", "access"]):
-            access_token = self.access_token
-            return True if access_token else False
-
-        elif scheme in set(["basic", "client"]):
-            client_id, client_secret = self.client_tokens
-            return True if (client_id and client_secret) else False
-
-
-class Response(Http):
-    """The Response object, every request instance that comes in will get a
-    corresponding Response instance that answers the Request.
-
-    an instance of this class is used to create the text response that will be sent 
-    back to the client
-
-    Request has a ._body and .body, the ._body property is the raw value that is
-    returned from the Controller method that handled the request, the .body property
-    is a string that is ready to be sent back to the client, so it is _body converted
-    to a string. The reason _body isn't name body_kwargs is because _body can be
-    almost anything (not just a dict)
-    """
-
-    encoding = ""
-
-    @property
-    def code(self):
-        """the http status code to return to the client, by default, 200 if a body is present otherwise 204"""
-        code = getattr(self, '_code', None)
-        if not code:
-            if self.has_body():
-                code = 200
-            else:
-                code = 204
-
-        return code
-
-    @code.setter
-    def code(self, v):
-        self._code = v
-        self._status = None
-
-    @property
-    def status_code(self): return self.code
-
-    @status_code.setter
-    def status_code(self, v): self.code = v
-
-    @property
-    def status(self):
-        """The full http status (the first line of the headers in a server response)"""
-        if not getattr(self, '_status', None):
-            c = self.code
-            status_tuple = BaseHTTPRequestHandler.responses.get(self.code)
-            msg = "UNKNOWN"
-            if status_tuple: msg = status_tuple[0]
-            self._status = msg
-
-
-        return self._status
-
-    @status.setter
-    def status(self, v):
-        self._status = v
-
-    @property
-    def body(self):
-        """return the body, formatted to the appropriate content type"""
-        return getattr(self, "_body", None)
-
-    @body.setter
-    def body(self, v):
-        self._body = v
-        if self.is_file():
-            filepath = getattr(v, "name", "")
-            if filepath:
-                mt = MimeType.find_type(filepath)
-                filesize = os.path.getsize(filepath)
-                self.set_header("Content-Type", mt)
-                self.set_header("Content-Length", filesize)
-                logger.debug(
-                    "Response body set to file: \"{}\" with mimetype: \"{}\" and size: {}".format(
-                        filepath,
-                        mt,
-                        filesize
-                    )
-                )
-
-            else:
-                logger.warn("Response body is a filestream that has no .filepath property")
-
-    def has_body(self):
-        """return True if there is an actual response body"""
-        return getattr(self, "_body", None) is not None
-
-    def is_file(self):
-        """return True if the response body is a file pointer"""
-        # http://stackoverflow.com/questions/1661262/check-if-object-is-file-like-in-python
-        return hasattr(self._body, "read") if self.has_body() else False
-
-    def set_cors_headers(self, request_headers, custom_response_headers=None):
-        allow_headers = request_headers['Access-Control-Request-Headers']
-        allow_method = request_headers['Access-Control-Request-Method']
-        origin = request_headers['origin']
-
-        cors_headers = {
-            'Access-Control-Allow-Origin': origin,
-            'Access-Control-Allow-Credentials': 'true',
-            'Access-Control-Allow-Methods': allow_method,
-            'Access-Control-Allow-Headers': allow_headers,
-            'Access-Control-Max-Age': 3600
-        }
-
-        if custom_response_headers:
-            cors_headers.update(custom_response_headers)
-
-        self.add_headers(cors_headers)
-
-    def is_success(self):
-        """return True if this response is considered a "successful" response"""
-        code = self.code
-        return code < 400
-    is_successful = is_success
-
-
diff --git a/endpoints/interface/__init__.py b/endpoints/interface/__init__.py
index 9713eb5..633f866 100644
--- a/endpoints/interface/__init__.py
+++ b/endpoints/interface/__init__.py
@@ -1,337 +1,2 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-import logging
-import json
-import sys
-from functools import partial
-
-from ..http import Request, Response
-from .. import environ
-from ..call import Router, Call
-from ..decorators import property
-from ..exception import CallError, Redirect, CallStop, AccessDenied
-from ..utils import ByteString, JSONEncoder
-
-
-logger = logging.getLogger(__name__)
-
-
-class Payload(object):
-    """We have a problem with generic websocket support, websockets don't have an
-    easy way to decide how they will send stuff back and forth like http (where you
-    can set a content type header and things like that) so Payload solves that problem,
-    the websocket client and the default interface of the servers will use Payload but
-    it could be overridden and completely changed and then you would just need to set
-    the class in the client and server interfaces and you could completely change the
-    way the websockets interact with each other
-
-    I thought about doing something with the accept header or content-type on a Request
-    object, which maybe would work but you can also override the Request and Response
-    objects and that would mean you would need to put the loads/dumps code in possibly
-    two different places, which I don't love because I like DRY solutions so it would
-    still be great to have a class like this that all the other interfaces wrap
-    in order to send/receive data via websockets
-    """
-    @classmethod
-    def loads(cls, raw):
-        return json.loads(raw)
-
-    @classmethod
-    def dumps(cls, kwargs):
-        return json.dumps(kwargs, cls=JSONEncoder)
-
-
-class BaseServer(object):
-    """all servers should extend this and implemented the NotImplemented methods,
-    this ensures a similar interface among all the different servers
-
-    A server is different from the interface because the server is actually responsible
-    for serving the requests, while the interface will translate the requests to
-    and from endpoints itself into something the server backend can understand
-    """
-    controller_prefixes = None
-    """the controller prefixes (python module paths) you want to use to find your Controller subclasses"""
-
-    backend_class = None
-    """the supported server's interface, there is no common interface for this class.
-    Basically it is the raw backend class that the BaseServer child is translating
-    for endpoints compatibility"""
-
-    request_class = Request
-    """the endpoints.http.Request compatible class that should be used to make
-    Request() instances"""
-
-    response_class = Response
-    """the endpoints.http.Response compatible class that should be used to make
-    Response() instances"""
-
-    router_class = Router
-    """the endpoints.call.Router compatible class that handles translating a request
-    into the Controller class and method that will actual run"""
-
-    call_class = Call
-    """the endpoints.call.Call compatible class that should be used to make a
-    Call() instance"""
-
-    connection_class = None
-    """the endpoints.interface.BaseConnection compatible class that is used for long
-    running connections like websockets"""
-
-    @property
-    def hostloc(self):
-        """Return host:port string that the server is using to answer requests"""
-        raise NotImplementedError()
-
-    @property(cached="_backend")
-    def backend(self):
-        ret = self.create_backend()
-        return ret
-
-    def __init__(self, controller_prefixes=None, **kwargs):
-        if controller_prefixes:
-            self.controller_prefixes = controller_prefixes
-        else:
-            if "controller_prefix" in kwargs:
-                self.controller_prefixes = [kwargs["controller_prefix"]]
-            else:
-                self.controller_prefixes = environ.get_controller_prefixes()
-
-        for k, v in kwargs.items():
-            if k.endswith("_class"):
-                setattr(self, k, v)
-
-    def create_connection(self, **kwargs):
-        return self.connection_class(self, **kwargs)
-
-    def create_backend(self, **kwargs):
-        """create instance of the backend class.
-
-        Endpoints works by translating incoming requests from this instance to something
-        endpoints understands in create_request() and then translating the response
-        from endpoints back into something the backend understands in handle_request()
-
-        :returns: mixed, an instance of the backend class
-        """
-        return self.backend_class(**kwargs)
-
-    def create_request(self, raw_request, **kwargs):
-        """convert the raw interface raw_request to a request that endpoints understands
-
-        :params raw_request: mixed, this is the request given by backend
-        :params **kwargs:
-            :returns: an http.Request instance that endpoints understands
-        """
-        raise NotImplementedError()
-
-    def create_request_body(self, request, raw_request, **kwargs):
-        raise NotImplementedError()
-
-    def create_response(self, **kwargs):
-        """create the endpoints understandable response instance that is used to
-        return output to the client"""
-        return self.response_class()
-
-    def create_response_body(self, response, json_encoder=JSONEncoder, **kwargs):
-        """usually when iterating this object it means we are returning the response
-        of a wsgi request, so this will iterate the body and make sure it is a bytes
-        string because wsgiref requires an actual bytes instance, a child class won't work
-
-        :returns: a generator that yields bytes strings
-        """
-        if not response.has_body(): return
-
-        body = response.body
-
-        if response.is_file():
-            if body.closed:
-                raise IOError("cannot read streaming body because pointer is closed")
-
-            # http://stackoverflow.com/questions/15599639/whats-perfect-counterpart-in-python-for-while-not-eof
-            for chunk in iter(partial(body.read, 8192), ''):
-                yield ByteString(chunk, response.encoding).raw()
-
-            # close the pointer since we've consumed it
-            body.close()
-
-        elif response.is_json():
-            # TODO ???
-            # I don't like this, if we have a content type but it isn't one
-            # of the supported ones we were returning the exception, which threw
-            # Jarid off, but now it just returns a string, which is not best either
-            # my thought is we could have a body_type_subtype method that would 
-            # make it possible to easily handle custom types
-            # eg, "application/json" would become: self.body_application_json(b, is_error)
-            body = json.dumps(body, cls=json_encoder)
-            yield ByteString(body, response.encoding).raw()
-
-        else:
-            # just return a string representation of body if no content type
-            yield ByteString(body, response.encoding).raw()
-
-    def create_call(self, raw_request, request=None, response=None, router=None, **kwargs):
-        """create a call object that has endpoints understandable request and response
-        instances"""
-        req = request if request else self.create_request(raw_request, **kwargs)
-        res = response if response else self.create_response(**kwargs)
-        rou = router if router else self.create_router(**kwargs)
-        c = self.call_class(req, res, rou)
-        return c
-
-    def create_router(self, **kwargs):
-        kwargs.setdefault('controller_prefixes', self.controller_prefixes)
-        r = self.router_class(**kwargs)
-        return r
-
-    def handle_request(self):
-        """this should be able to get a raw_request, pass it to create_call(),
-        then use the Call instance to handle the request, and then send a response
-        back to the backend
-        """
-        raise NotImplementedError()
-
-    def serve_forever(self):
-        try:
-            while True: self.handle_request()
-        except Exception as e:
-            logger.exception(e)
-            raise
-
-    def serve_count(self, count):
-        try:
-            handle_count = 0
-            while handle_count < count:
-                self.handle_request()
-                handle_count += 1
-        except Exception as e:
-            logger.exception(e)
-            raise
-
-
-class BaseWebsocketServer(BaseServer):
-
-    payload_class = Payload
-
-    def create_websocket_request(self, request, raw_request=None):
-        """create the websocket Request for this call using the original Request
-        instance from the initial ws connection
-
-        :param request: the original Request instance from the initial connection
-        :param raw_request: this will be passed to self.payload_class to be 
-            interpretted
-        :returns: a new Request instance to be used for this specific call
-        """
-        ws_req = request.copy()
-        ws_req.controller_info = None
-
-        # just in case we need access to the original request object or the raw info
-        ws_req.parent = request
-        ws_req.raw_request = raw_request
-
-        if raw_request:
-            # path, body, method, uuid
-            kwargs = self.payload_class.loads(raw_request)
-            #kwargs.setdefault("body", None)
-            kwargs.setdefault("path", request.path)
-            kwargs.setdefault("headers", {})
-            kwargs.setdefault("body", {})
-
-            ws_req.environ["REQUEST_METHOD"] = kwargs["method"]
-            ws_req.method = kwargs["method"]
-
-            ws_req.environ["PATH_INFO"] = kwargs["path"]
-            ws_req.path = kwargs["path"]
-
-            ws_req.environ.pop("wsgi.input", None)
-
-            ws_req.body = kwargs["body"]
-            ws_req.body_kwargs = kwargs["body"]
-
-            #ws_req.uuid = kwargs["uuid"] if "uuid" in kwargs else None
-            ws_req.uuid = kwargs.get("uuid", request.uuid)
-
-            ws_req.headers.update(kwargs["headers"])
-
-        return ws_req
-
-    def create_websocket_response_body(self, request, response, json_encoder=JSONEncoder, **kwargs):
-        """Similar to create_response_body it prepares a response to be sent back
-        down the wire using the payload_class variable
-
-        :param request: the call's Request instance, this needs the request because
-            of how websockets are sent back and forth
-        :param response: the call's Response instance
-        :returns: a generator that yields bytes strings
-        """
-        raw_response = {}
-
-        raw_response["path"] = request.path
-        if request.uuid:
-            raw_response["uuid"] = request.uuid
-
-        raw_response["code"] = response.code
-        raw_response["body"] = response.body
-
-        body = self.payload_class.dumps(raw_response)
-        yield ByteString(body, response.encoding).raw()
-
-    def connect_websocket_call(self, raw_request):
-        """called during websocket handshake
-
-        this should modify the request instance to use the CONNECT method so you
-        can customize functionality in your controller using a CONNECT method
-
-        NOTE -- this does not call create_websocket_request, it does call create_request
-
-        :param raw_request: the raw request from the backend
-        :returns: Call instance that can handle the request
-        """
-        c = self.create_call(raw_request)
-        req = c.request
-
-        # if there is an X-uuid header then set uuid and send it down
-        # with every request using that header
-        # https://stackoverflow.com/questions/18265128/what-is-sec-websocket-key-for
-        uuid = None
-
-        # first try and get the uuid from the body since javascript has limited
-        # capability of setting headers for websockets
-        kwargs = req.kwargs
-        if "uuid" in kwargs:
-            uuid = kwargs["uuid"]
-
-        # next use X-UUID header, then the websocket key
-        if not uuid:
-            uuid = req.find_header(["X-UUID", "Sec-Websocket-Key"])
-
-        req.uuid = uuid
-        req.method = "CONNECT"
-        return c
-
-    def create_websocket_call(self, request, raw_request=None):
-        """for every message sent back and forth over the websocket this should be
-        called
-
-        :param request: Request, the main request from the initial ws connection
-        :param raw_request: mixed, the raw request pulled from some backend that
-            should be acted on
-        :returns: Call instance
-        """
-        req = self.create_websocket_request(request, raw_request)
-        c = self.create_call(raw_request, request=req)
-        return c
-
-    def disconnect_websocket_call(self, request):
-        """This handles a websocket disconnection
-
-        this should modify the request instance to use the DISCONNECT method so you
-        can customize functionality in your controller using a DISCONNECT method
-
-        :param request: Request, the main request from the initial ws connection
-        :returns: Call instance
-        """
-        #req = self.create_websocket_request(request)
-        request.method = "DISCONNECT"
-        c = self.create_call(None, request=request)
-        return c
 
diff --git a/endpoints/interface/asgi.py b/endpoints/interface/asgi.py
new file mode 100644
index 0000000..66c4d52
--- /dev/null
+++ b/endpoints/interface/asgi.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+
+from ..compat import *
+from .base import BaseApplication
+from ..utils import String
+
+
+class Application(BaseApplication):
+    """ASGI HTTP and WebSocket Interface support
+
+    Good intro tutorial:
+        https://www.encode.io/articles/asgi-http
+
+    HTTP lifecycle:
+        https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
+
+    WebSocket lifecycle:
+        https://asgi.readthedocs.io/en/latest/specs/www.html#websocket-connection-scope
+    """
+    def normalize_call_kwargs(self, scope, receive, send, **kwargs):
+        return {
+            "scope": scope,
+            "receive": receive,
+            "send": send,
+            **kwargs
+        }
+
+    def is_http_call(self, scope, **kwargs):
+        return scope["type"] == "http"
+
+    def is_websocket_call(self, scope, **kwargs):
+        return scope["type"] == "websocket"
+
+    async def handle_http(self, **kwargs):
+        request = await self.create_request(**kwargs)
+
+        d = await self.recv_websocket(**kwargs)
+        body = d["body"]
+        while d["more_body"]:
+            d = await self.recv_websocket(**kwargs)
+            body += d["body"]
+
+        await self.set_request_body(request, body, **kwargs)
+
+        response = await self.create_response()
+        await self.handle(request, response)
+
+        await kwargs["send"]({
+            "type": "http.response.start",
+            "status": response.code,
+            "headers": list(response.headers.asgi()),
+        })
+
+        # https://peps.python.org/pep-0525/
+        # https://stackoverflow.com/a/37550568
+        async for body in self.get_response_body(response):
+            await kwargs["send"]({
+                "type": "http.response.body",
+                "body": body,
+                "more_body": True,
+            })
+
+        await kwargs["send"]({
+            "type": "http.response.body",
+            "body": b"",
+            "more_body": False,
+        })
+
+    def is_websocket_recv(self, data, **kwargs):
+        return data["type"] == "websocket.receive"
+
+    def is_websocket_close(self, data, **kwargs):
+        return data["type"] == "websocket.disconnect"
+
+    async def handle_websocket_recv(self, data, **kwargs):
+        # https://asgi.readthedocs.io/en/latest/specs/www.html#receive-receive-event
+        request = await self.create_request(**kwargs)
+        response = await self.create_response()
+
+        d = self.get_websocket_loads(data["text"])
+
+        for k in ["path", "uuid", "method"]:
+            if k in d:
+                setattr(request, k, d[k])
+
+        if "body" in d:
+            await self.set_request_body(request, d["body"])
+
+        if d["headers"]:
+            request.add_headers(d["headers"])
+
+        await self.handle(request, response, **kwargs)
+        await self.send_websocket(request, response, **kwargs)
+
+    async def recv_websocket(self, **kwargs):
+        return await kwargs["receive"]()
+
+    async def send_websocket(self, request, response, **kwargs):
+        d = {
+            "type": "websocket.send",
+            "text": self.get_websocket_dumps(
+                uuid=request.uuid,
+                code=response.code,
+                path=request.path,
+                body=response.body,
+            )
+        }
+        await kwargs["send"](d)
+
+    async def handle_websocket_connect(self, **kwargs):
+        d = await self.recv_websocket(**kwargs)
+        if d["type"] == "websocket.connect":
+            await super().handle_websocket_connect(**kwargs)
+
+        else:
+            response = await self.create_response(**kwargs)
+            response.code = 1002
+            await self.send_websocket_connect(None, response, **kwargs)
+
+    async def send_websocket_connect(self, request, response, **kwargs):
+        if response.is_success():
+            await kwargs["send"]({
+                "type": "websocket.accept",
+            })
+            await self.send_websocket(request, response, **kwargs)
+
+        else:
+            await self.send_websocket_disconnect(request, response, **kwargs)
+
+    async def send_websocket_disconnect(self, request, response, **kwargs):
+        body = b""
+        async for part in self.get_response_body(response):
+            body += part
+
+        r = await kwargs["send"]({
+            "type": "websocket.close",
+            "code": response.code,
+            "reason": String(body),
+        })
+
+    async def create_request(self, **kwargs):
+        raw_request = kwargs["scope"]
+        request = self.request_class()
+        request.add_headers(raw_request.get("headers", []))
+
+        request.path = raw_request['path']
+        request.query = raw_request['query_string']
+        request.host, request.port = raw_request["server"]
+
+        if self.is_http_call(raw_request):
+            request.method = raw_request['method']
+            request.scheme = raw_request.get("scheme", "http")
+
+        elif self.is_websocket_call(raw_request):
+            request.scheme = raw_request.get("scheme", "ws")
+
+        request.raw_request = raw_request
+        return request
+
+
+class ApplicationFactory(object):
+    """This is convenience wrapper to make it easy to call daphne, because you
+    can just do this:
+
+        $ daphne -b 0.0.0.0 -p 4000 -v 3 endpoints.interface.asgi.ApplicationFactory
+    """
+    application = None
+    """Will hold a cached instance of .application_class"""
+
+    application_class = Application
+    """The application class that will be created"""
+
+    @classmethod
+    def get_application(cls, **kwargs):
+        if not cls.application:
+            cls.application = cls.application_class(**kwargs)
+        return cls.application
+
+    def __init__(self, scope, **kwargs):
+        self.scope = scope
+
+    async def __call__(self, receive, send):
+        application = self.get_application()
+        await application(self.scope, receive, send)
+
diff --git a/endpoints/interface/base.py b/endpoints/interface/base.py
new file mode 100644
index 0000000..d971119
--- /dev/null
+++ b/endpoints/interface/base.py
@@ -0,0 +1,786 @@
+# -*- coding: utf-8 -*-
+import logging
+import json
+import functools
+import io
+import email
+import time
+import inspect
+
+from datatypes import String, ReflectModule
+
+from ..config import environ
+from ..call import Controller, Request, Response
+from ..exception import (
+    CallError,
+    Redirect,
+    CallStop,
+    AccessDenied,
+    RouteError,
+    VersionError,
+    CloseConnection,
+)
+
+from ..utils import ByteString, JSONEncoder
+
+
+logger = logging.getLogger(__name__)
+
+
+class ApplicationABC(object):
+    """Child classes should extend BaseApplication but this class contains
+    the methods that a child interface will most likely want to override so
+    they are all here for convenience"""
+    def normalize_call_kwargs(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    async def handle_http(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    async def create_request(self, raw_request, **kwargs):
+        raise NotImplementedError()
+
+    def is_http_call(self, *args, **kwargs):
+        return True
+
+    def is_websocket_call(self, *args, **kwargs):
+        return False
+
+    def is_websocket_recv(self, data, **kwargs):
+        raise NotImplementedError()
+
+    def is_websocket_close(self, data, **kwargs):
+        raise NotImplementedError()
+
+    async def handle_websocket_recv(self, data, **kwargs):
+        raise NotImplementedError()
+
+    async def send_websocket(self, request, response, **kwargs):
+        raise NotImplementedError()
+
+    async def handle_websocket_connect(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    async def send_websocket_connect(self, request, response, **kwargs):
+        raise NotImplementedError()
+
+    async def handle_websocket_disconnect(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    async def send_websocket_disconnect(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    async def recv_websocket(self, *args, **kwargs):
+        raise NotImplementedError()
+
+
+class BaseApplication(ApplicationABC):
+    """all servers should extend this and implemented the NotImplemented methods,
+    this ensures a similar interface among all the different servers
+
+    A server is different from the interface because the server is actually
+    responsible for serving the requests, while the interface will translate the
+    requests to and from endpoints itself into something the server backend can
+    understand
+
+    webSocket protocol: https://www.rfc-editor.org/rfc/rfc6455
+
+    """
+    controller_prefixes = None
+    """the controller prefixes (python module paths) you want to use to find your
+    Controller subclasses"""
+
+    request_class = Request
+    """the endpoints.http.Request compatible class that should be used to make
+    Request() instances"""
+
+    response_class = Response
+    """the endpoints.http.Response compatible class that should be used to make
+    Response() instances"""
+
+    controller_class = Controller
+    """Every defined controller has to be a child of this class"""
+
+    def __init__(self, controller_prefixes=None, **kwargs):
+        if controller_prefixes:
+            self.controller_prefixes = controller_prefixes
+
+        else:
+            if "controller_prefix" in kwargs:
+                self.controller_prefixes = [kwargs["controller_prefix"]]
+
+            else:
+                self.controller_prefixes = environ.get_controller_prefixes()
+
+        for k, v in kwargs.items():
+            if k.endswith("_class"):
+                setattr(self, k, v)
+
+    async def __call__(self, *args, **kwargs):
+        """this is what will be called for each request that the server handles
+        """
+        call_kwargs = self.normalize_call_kwargs(*args, **kwargs)
+
+        if self.is_http_call(**call_kwargs):
+            return await self.handle_http(**call_kwargs)
+
+        elif self.is_websocket_call(**call_kwargs):
+            return await self.handle_websocket(**call_kwargs)
+
+        else:
+            logger.warning("Request was not HTTP or WebSocket")
+
+    async def create_request(self, raw_request, **kwargs):
+        """convert the raw interface raw_request to a request that endpoints
+        understands
+
+        :params raw_request: mixed, this is the request given by backend
+        :params **kwargs:
+            :returns: an http.Request instance that endpoints understands
+        """
+        request = self.request_class()
+        request.raw_request = raw_request
+        return request
+
+    async def set_request_body(self, request, body, **kwargs):
+        """
+        :returns: tuple[Any, list, dict], returns raw_body, body_args, and
+            body_kwargs
+        """
+        if request.headers.is_chunked():
+            raise IOError("Chunked bodies are not supported")
+
+        if isinstance(body, io.IOBase):
+            length = int(request.get_header(
+                "Content-Length",
+                -1,
+                allow_empty=False
+            ))
+            if length > 0:
+                body = body.read(length)
+
+            else:
+                # since there is no content length we can conclude that we don't
+                # actually have a body
+                body = None
+
+        args = []
+        kwargs = {}
+
+        if body:
+            if isinstance(body, dict):
+                kwargs = body
+
+            elif isinstance(body, list):
+                args = body
+
+            elif request.headers.is_json():
+                jb = json.loads(body)
+
+                if isinstance(jb, dict):
+                    kwargs = jb
+
+                elif isinstance(jb, list):
+                    args = jb
+
+                else:
+                    args = [jb]
+
+            elif request.headers.is_urlencoded():
+                kwargs = request._parse_query_str(body)
+
+            elif request.headers.is_multipart():
+                em = email.message_from_bytes(bytes(request.headers) + body)
+                for part in em.walk():
+                    if not part.is_multipart():
+                        data = part.get_payload(decode=True)
+                        params = {}
+                        for header_name in part:
+                            for k, v in part.get_params(header=header_name)[1:]:
+                                params[k] = v
+
+                        if "name" not in params:
+                            raise IOError("Bad body data")
+
+                        if "filename" in params:
+                            fp = io.BytesIO(data)
+                            fp.filename = params["filename"]
+                            kwargs[params["name"]] = fp
+
+                        else:
+                            kwargs[params["name"]] = String(data)
+
+            elif request.headers.is_plain():
+                body = String(body, encoding=request.encoding)
+
+        request.body = body
+        request.body_args = args
+        request.body_kwargs = kwargs
+
+    async def create_response(self, **kwargs):
+        """create the endpoints understandable response instance that is used to
+        return output to the client"""
+        return self.response_class()
+
+    async def get_response_body(self, response, json_encoder=JSONEncoder, **kwargs):
+        """usually when iterating this object it means we are returning the
+        response of a wsgi request, so this will iterate the body and make sure
+        it is a bytes string because wsgiref requires an actual bytes instance,
+        a child class won't work
+
+        :returns: a generator that yields bytes strings
+        """
+        if response.has_body():
+
+            body = response.body
+
+            if response.is_file():
+                if body.closed:
+                    raise IOError(
+                        "cannot read streaming body because pointer is closed"
+                    )
+
+                # http://stackoverflow.com/questions/15599639/
+                for chunk in iter(functools.partial(body.read, 8192), ''):
+                    yield ByteString(chunk, response.encoding).raw()
+
+                # close the pointer since we've consumed it
+                body.close()
+
+            elif response.is_json():
+                # TODO ???
+                # I don't like this, if we have a content type but it isn't one
+                # of the supported ones we were returning the exception, which
+                # threw Jarid off, but now it just returns a string, which is not
+                # best either my thought is we could have a body_type_subtype
+                # method that would make it possible to easily handle custom
+                # types, eg, "application/json" would become:
+                #    self.body_application_json(b, is_error)
+                body = json.dumps(body, cls=json_encoder)
+                yield ByteString(body, response.encoding).raw()
+
+            else:
+                # just return a string representation of body if no content type
+                yield ByteString(body, response.encoding).raw()
+
+    @functools.cache
+    def get_controller_module_paths(self):
+        """get all the modules in the controller_prefixes
+
+        :returns: set, a set of string module names (eg foo.bar, foo.che)
+        """
+        controller_modpaths = set()
+        for controller_prefix in self.controller_prefixes:
+            rm = ReflectModule(controller_prefix)
+            controller_modpaths.update(rm.module_names())
+
+        return controller_modpaths
+
+    async def get_controller_class(self, module, class_name):
+        """try and get the class_name from the module and make sure it is a
+        valid controller"""
+        class_name = class_name.capitalize()
+        class_object = getattr(module, class_name, None)
+        if not class_object or not issubclass(class_object, Controller):
+            class_object = None
+
+        return class_object
+
+    async def find_controller_class(self, module, path_args, **kwargs):
+        named_class = None
+        default_class = None
+
+        if path_args:
+            # look for a class name with the first path arg, this will be
+            # used if a matching module isn't found
+            named_class = await self.get_controller_class(
+                module,
+                path_args[0]
+            )
+
+        if not named_class:
+            # look for the default class just in case, this is a first
+            # match wins scenario. Basically, the first controller default
+            # class found will be the class that answers the call unless
+            # a class matching a path arg is found
+            default_class = await self.get_controller_class(
+                module,
+                kwargs.get("class_fallback", "Default"),
+            )
+
+        return named_class, default_class
+
+    async def find_controller_info(self, request, **kwargs):
+        """returns all the information needed to create a controller and handle
+        the request
+
+        This is where all the routing magic happens, this takes the request.path
+        and gathers the information needed to turn that path into a Controller
+
+        we always translate an HTTP request using this pattern:
+
+            METHOD /module/class/args?kwargs
+
+        GET /foo -> controller_prefix.foo.Default.GET
+        POST /foo/bar -> controller_prefix.foo.Bar.POST
+        GET /foo/bar/che -> controller_prefix.foo.Bar.GET(che)
+        POST /foo/bar/che?baz=foo -> controller_prefix.foo.Bar.POST(che, baz=foo)
+
+        :param request: Request
+        :param **kwargs:
+            * class_fallback: str, the name of the default controller class, it
+                defaults to Default and should probably never be changed
+            * method_fallback: str, the name of the fallback controller method,
+                it defaults to ANY and should probably never be changed
+        :returns: dict
+        """
+        logger.debug("Compiling Controller info using path: {}".format(
+            request.path
+        ))
+
+        ret = {
+            "module_name": "",
+            "module_path_args": [],
+            "controller_path_args": [],
+        }
+        default_ret = {}
+        named_ret = {}
+
+        path_args = list(request.path_args)
+
+        controller_modpaths = self.get_controller_module_paths()
+        for controller_prefix in self.controller_prefixes:
+            modpath = controller_prefix
+            # using the path_args we are going to try and find the best module
+            # path for the request
+            while path_args:
+                modpath += "." + path_args[0]
+                if modpath in controller_modpaths:
+                    ret["module_name"] = modpath
+                    ret["module_path_args"].append(path_args[0])
+                    ret["controller_path_args"].append(path_args.pop(0))
+                    ret["controller_prefix"] = controller_prefix
+
+                else:
+                    break
+
+            if ret["module_name"]:
+                ret["module"] = ReflectModule(ret["module_name"]).get_module()
+
+                named_class, default_class = await self.find_controller_class(
+                    ret["module"],
+                    path_args,
+                )
+
+                if named_class:
+                    ret["class"] = named_class
+                    ret["controller_path_args"].append(path_args.pop(0))
+
+                elif default_class:
+                    ret["class"] = default_class
+
+                else:
+                    raise TypeError(
+                        " ".join([
+                            "Could not find a valid module and Controller",
+                            f"class for {request.path}"
+                        ])
+                    )
+
+                break
+
+            else:
+                # we didn't find the correct module using module paths, so now
+                # let's try class paths, first found class path wins
+                if not named_ret or not default_ret:
+                    controller_module = ReflectModule(
+                        controller_prefix
+                    ).get_module()
+
+                    named_class, default_class = await self.find_controller_class(
+                        controller_module,
+                        path_args,
+                    )
+
+                    if named_class and not named_ret:
+                        named_ret["class"] = named_class
+                        named_ret["module"] = controller_module
+                        named_ret["module_name"] = controller_prefix
+                        named_ret["controller_prefix"] = controller_prefix
+                        named_ret["controller_path_args"] = [path_args.pop(0)]
+
+                    if default_class and not default_ret:
+                        default_ret["class"] = default_class
+                        default_ret["module"] = controller_module
+                        default_ret["module_name"] = controller_prefix
+                        default_ret["controller_prefix"] = controller_prefix
+
+        if not ret["module_name"]:
+            if named_ret:
+                ret.update(named_ret)
+
+            elif default_ret:
+                ret.update(default_ret)
+
+            else:
+                raise TypeError(
+                    " ".join([
+                        "Could not find a valid module with path",
+                        "{} and controller_prefixes {}".format(
+                            request.path,
+                            self.controller_prefixes
+                        )
+                    ])
+                )
+
+        ret["method_args"] = path_args
+        # we merge the leftover path args with the body kwargs
+        ret['method_args'].extend(request.body_args)
+
+        ret['method_kwargs'] = request.kwargs
+
+        ret["method_prefix"] = request.method.upper()
+        ret["method_fallback"] = kwargs.get("method_fallback", "ANY")
+
+        ret['module_path'] = "/".join(ret["module_path_args"])
+        ret['class_name'] = ret["class"].__name__
+        ret['class_path'] = "/".join(ret["controller_path_args"])
+
+        return ret
+
+    async def create_controller(self, request, response, **kwargs):
+        """Create a controller to handle the request
+
+        :returns: Controller, this Controller instance should be able to handle
+            the request
+        """
+        try:
+            request.controller_info = await self.find_controller_info(
+                request,
+                **kwargs
+            )
+
+        except (ImportError, AttributeError, TypeError) as e:
+            raise CallError(
+                404,
+                "Path {} could not resolve to a controller".format(
+                    request.path,
+                ) 
+            ) from e
+
+        else:
+            controller = request.controller_info['class'](
+                request,
+                response,
+                **kwargs
+            )
+
+        return controller
+
+    async def handle(self, request, response, **kwargs):
+        """Called from the interface to actually handle the request."""
+        controller = None
+        start = time.time()
+
+        try:
+            controller = await self.create_controller(request, response)
+            controller.log_start(start)
+
+            controller_args = request.controller_info["method_args"]
+            controller_kwargs = request.controller_info["method_kwargs"]
+            controller_method = getattr(controller, "handle")
+
+            logger.debug("Request handle method: {}.{}.{}".format(
+                controller.__class__.__module__,
+                controller.__class__.__name__,
+                controller_method.__name__
+            ))
+            await controller_method(*controller_args, **controller_kwargs)
+
+        except Exception as e:
+            await self.handle_error(
+                request,
+                response,
+                e,
+                controller,
+                **kwargs
+            )
+
+        finally:
+            if response.code == 204:
+                response.headers.pop('Content-Type', None)
+                response.body = None # just to be sure since body could've been ""
+
+            if controller:
+                controller.log_stop(start)
+
+    async def handle_error(self, req, res, e, controller, **kwargs):
+        """if an exception is raised while trying to handle the request it will
+        go through this method
+
+        This method will set the response body and then also calls
+        Controller.handle_error for further customization if the Controller is
+        available
+
+        :param e: Exception, the error that was raised
+        :param **kwargs: dict, any other information that might be handy
+        """
+        res.error = e
+
+        if isinstance(e, CloseConnection):
+            raise
+
+        res.body = e
+
+        if isinstance(e, CallStop):
+            logger.debug(String(e))
+            res.code = e.code
+            res.add_headers(e.headers)
+            res.body = e.body
+
+        elif isinstance(e, Redirect):
+            logger.debug(String(e))
+            res.code = e.code
+            res.add_headers(e.headers)
+            res.body = None
+
+        elif isinstance(e, (AccessDenied, CallError)):
+            logger.warning(String(e))
+            res.code = e.code
+            res.add_headers(e.headers)
+
+        elif isinstance(e, NotImplementedError):
+            logger.warning(String(e))
+            res.code = 501
+
+        elif isinstance(e, TypeError):
+            e_msg = String(e)
+            controller_info = req.controller_info
+
+            # filter out TypeErrors raised from non handler methods
+            correct_prefix = controller_info["method_name"] in e_msg
+            if correct_prefix and 'argument' in e_msg:
+                # there are subtle messaging differences between py2 and py3
+                errs = [
+                    "takes exactly",
+                    "takes no arguments",
+                    "positional argument"
+                ]
+                if (errs[0] in e_msg) or (errs[1] in e_msg) or (errs[2] in e_msg):
+                    # TypeError: <METHOD>() takes exactly M argument (N given)
+                    # TypeError: <METHOD>() takes no arguments (N given)
+                    # TypeError: <METHOD>() takes M positional arguments but N were given
+                    # TypeError: <METHOD>() takes 1 positional argument but N were given
+                    # we shouldn't ever get the "takes no arguments" case because of self,
+                    # but just in case
+                    # check if there are path args, if there are then 404, if not then 405
+                    #logger.debug(e_msg, exc_info=True)
+                    logger.debug(e_msg)
+
+                    if len(controller_info["method_args"]):
+                        res.code = 404
+
+                    else:
+                        res.code = 405
+
+                elif "unexpected keyword argument" in e_msg:
+                    # TypeError: <METHOD>() got an unexpected keyword argument '<NAME>'
+
+                    try:
+                        # if the binding of just the *args works then the
+                        # problem is the **kwargs so a 405 is appropriate,
+                        # otherwise return a 404
+                        inspect.getcallargs(
+                            controller_info["method"],
+                            *controller_info["method_args"]
+                        )
+                        res.code = 405
+
+                        logger.warning("Controller method {}.{}.{}".format(
+                            controller_info['module_name'],
+                            controller_info['class_name'],
+                            e_msg
+                        ))
+
+                    except TypeError:
+                        res.code = 404
+
+                elif "multiple values" in e_msg:
+                    # TypeError: <METHOD>() got multiple values for keyword argument '<NAME>'
+                    try:
+                        inspect.getcallargs(
+                            controller_info["method"],
+                            *controller_info["method_args"]
+                        )
+                        res.code = 409
+                        logger.warning(e)
+
+                    except TypeError:
+                        res.code = 404
+
+                else:
+                    res.code = 500
+                    logger.exception(e)
+
+            else:
+                res.code = 500
+                logger.exception(e)
+
+        else:
+            res.code = 500
+            logger.exception(e)
+
+        if controller:
+            error_method = getattr(
+                controller,
+                "handle_{}_error".format(res.code),
+                None
+            )
+            if not error_method:
+                error_method = getattr(
+                    controller,
+                    "handle_{}_error".format(req.method),
+                    None
+                )
+                if not error_method:
+                    error_method = getattr(controller, "handle_error")
+
+            logger.debug("Handle {} error using method: {}.{}".format(
+                res.code,
+                controller.__class__.__name__,
+                error_method.__name__
+            ))
+            await error_method(e, **kwargs)
+
+    @classmethod
+    def get_websocket_dumps(cls, **kwargs):
+        """Similar to create_response_body it prepares a response to be sent
+        back down the wire using the payload_class variable
+
+        This isn't asyncronouse because it is used in ..client.WebSocketClient
+        to create websocket bodies
+
+        This is the sister method to .get_websocket_loads() and should be a
+        mirror of that method
+
+        :param request: the call's Request instance, this needs the request
+            because of how websockets are sent back and forth
+        :param response: the call's Response instance
+        :returns: str
+        """
+        d = {}
+
+        d["path"] = kwargs["path"]
+
+        if uuid := kwargs.get("uuid", ""):
+            d["uuid"] = uuid
+
+        if code := kwargs.get("code", 0):
+            d["code"] = code
+
+        if method := kwargs.get("method", ""):
+            d["method"] = method
+
+        if "code" not in d and "method" not in d:
+            raise ValueError("A websocket payload needs a method or code")
+
+        if headers := kwargs.get("headers", {}):
+            d["headers"] = headers
+
+        body = kwargs.get("body", None)
+        if body is not None:
+            d["body"] = body
+
+        body = json.dumps(
+            d,
+            cls=kwargs.get("json_encoder", JSONEncoder),
+        )
+        return body
+
+    @classmethod
+    def get_websocket_loads(cls, body):
+        """Given a received websocket body this will convert it back into a dict
+
+        This isn't asyncronouse because it is used in ..client.WebSocketClient
+        to read websocket bodies sent from the server
+
+        This is the sister method to .get_websocket_dumps() and should be a
+        mirror of that method
+
+        :param body: str
+        :returns: dict
+        """
+        d = json.loads(body)
+
+        if "code" not in d and "method" not in d:
+            raise ValueError("A websocket payload needs a method or code")
+
+        if "path" not in d:
+            raise ValueError("A websocket payload must have a path")
+
+        if "body" not in d:
+            d["body"] = None
+
+        if "headers" not in d:
+            d["headers"] = {}
+
+        return d
+
+    async def handle_websocket_connect(self, **kwargs):
+        """This handles calling <FOUND CONTROLLER>.CONNECT
+        """
+        request = await self.create_request(**kwargs)
+        response = await self.create_response(**kwargs)
+
+        request.method = "CONNECT"
+
+        await self.handle(request, response, **kwargs)
+        await self.send_websocket_connect(request, response, **kwargs)
+
+    async def handle_websocket_disconnect(self, **kwargs):
+        """This handles calling <FOUND CONTROLLER>.DISCONNECT
+        """
+        request = await self.create_request(**kwargs)
+        response = await self.create_response(**kwargs)
+
+        response.code = kwargs.get("code", 1000)
+        request.method = "DISCONNECT"
+
+        await self.handle(request, response)
+        await self.send_websocket_disconnect(request, response, **kwargs)
+
+    async def handle_websocket(self, **kwargs):
+        """Handle the lifecycle of a websocket connection. Child interfaces
+        should override methods that this calls but shouldn't override this
+        method unless they really need to
+        """
+        await self.handle_websocket_connect(**kwargs)
+        disconnect = True
+
+        try:
+            while True:
+                data = await self.recv_websocket(**kwargs)
+
+                if self.is_websocket_recv(data, **kwargs):
+                    await self.handle_websocket_recv(data, **kwargs)
+
+                elif self.is_websocket_close(data, **kwargs):
+                    disconnect = False
+                    break
+
+                else:
+                    logger.warning("Websocket data was unrecognized")
+
+        except CloseConnection as e:
+            disconnect = True
+
+        except Exception as e:
+            # daphne was buring the error and I'm not sure why, so I'm going to
+            # leave this here for right now just in case
+            logger.exception(e)
+            raise
+
+        finally:
+            if disconnect:
+                await self.handle_websocket_disconnect(**kwargs)
+
diff --git a/endpoints/interface/client.py b/endpoints/interface/client.py
deleted file mode 100644
index bcd7b97..0000000
--- a/endpoints/interface/client.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import subprocess
-import os
-import sys
-import re
-import time
-import logging
-import threading
-from collections import deque
-
-from ..compat import *
-from .. import environ
-from ..utils import String, ByteString, Path
-from ..http import Url, Host
-from ..reflection import ReflectModule
-
-
-logger = logging.getLogger(__name__)
-
-
-class ServerThread(threading.Thread):
-    """Used internally by the Client classes to start one of the server clients
-    in a separate thread"""
-    def __init__(self, server):
-        super(ServerThread, self).__init__()
-        self.server = server
-        self.daemon = True
-        self.logger = server.logger
-
-    def flush(self, line):
-        self.server.logger.info("{:0>5}: {}".format(self.server.process.pid, String(line)))
-        #sys.stdout.write(String(line))
-        #sys.stdout.flush()
-
-    def run(self):
-        process = self.server.process
-        try:
-            # Poll process for new output until finished
-            for line in iter(process.stdout.readline, b""):
-                line = String(line).rstrip()
-                self.server.buf.append(line)
-                if not self.server.quiet:
-                    self.flush(line)
-
-        except Exception as e:
-            if not self.server.quiet:
-                self.flush(e)
-            raise
-
-        finally:
-            count = 0
-            if process:
-                try:
-                    process.terminate()
-
-                except OSError:
-                    pass
-
-                else:
-                    while count < 50:
-                        count += 1
-                        time.sleep(0.1)
-                        if process.poll() != None:
-                            break
-
-                    if process.poll() == None:
-                        process.kill()
-
-                finally:
-                    # fixes ResourceWarning: unclosed file <_io.BufferedReader name=4>
-                    # finally figured this out from captain, so I had evidently
-                    # hunted it down before
-                    process.stdout.close()
-
-
-class WebServer(object):
-    """This "client" is handy to get a simple testing server up and running
-
-    it is mainly handy for testing purposes, we found that we were copy/pasting
-    basically the same code to test random services and so it seemed like a good
-    idea to move the base code into endpoints so all our projects could share it.
-
-    :example:
-        server = WebServer("foo.bar")
-        server.start()
-    """
-
-    bufsize = 1000
-    """how many lines to buffer of output, set to 0 to suppress all output"""
-
-    quiet = False
-    """this is the default quiet setting for running a script, if False output is printed to stdout"""
-
-    host_regex = r"^Listening\s+on\s+(([^:]+):(\d+))$"
-    """This regex is used to find the correct host from the output from the server in find_host"""
-
-    @property
-    def logger(self):
-        logger = logging.getLogger("{}.WebServer".format(__name__))
-        if len(logger.handlers) == 0:
-            logger.setLevel(logging.INFO)
-            log_handler = logging.StreamHandler(stream=sys.stdout)
-            log_handler.setFormatter(logging.Formatter('%(message)s'))
-            logger.addHandler(log_handler)
-            logger.propagate = False
-        return logger
-
-    @property
-    def environ(self):
-        env = getattr(self, "_environ", None)
-        if env: return env
-
-        env = dict(os.environ)
-
-        # pwd needed when running endpoints as a local module (eg, python -m endpoints)
-        pwd = ReflectModule(__name__.split(".")[0]).path
-        pythonpath = pwd + os.pathsep + self.cwd
-        #pythonpath = self.cwd
-
-        if "PYTHONPATH" in env:
-            env["PYTHONPATH"] += os.pathsep + pythonpath
-        else:
-            env["PYTHONPATH"] = pythonpath
-
-        for env_name in environ.get_prefix_names("ENDPOINTS_PREFIX"):
-            env.pop(env_name)
-        env["ENDPOINTS_PREFIX"] = self.controller_prefix
-        self._environ = env
-
-        return env
-
-    @environ.setter
-    def environ(self, v):
-        self._environ = v
-
-    @environ.deleter
-    def environ(self):
-        del self._environ
-
-    @property
-    def output(self):
-        try:
-            ret = "\n".join(self.buf)
-        except AttributeError:
-            ret = ""
-        return ret
-
-    def __init__(self, controller_prefix, host="", config_path="", **kwargs):
-        """create a WSGI simple server
-
-        controller_prefix -- string -- the endpoints prefix, the value that would be passed
-            to ENDPOINTS_PREFIX
-        host -- string -- the hostname:port, something like 127.0.0.1:8080 or 0.0.0.0:8080 or localhost
-        :param config_path: a path to a .py file that contains configuration
-        **kwargs -- dict -- provides an easy hook to set other instance properties
-        """
-        self.controller_prefix = controller_prefix
-        if not host:
-            host = environ.HOST
-        self.host = Host(host).netloc if host else None
-
-        self.cwd = Path(kwargs.get("cwd", os.getcwd()))
-        self.config_path = config_path
-        self.environ = kwargs.get("environ", kwargs.get("env", {}))
-        self.process = None
-
-    def kill(self):
-        key = self.controller_prefix
-        cmd = "pkill -9 -f \"{}\"".format(key)
-        subprocess.call("{} > /dev/null 2>&1".format(cmd), shell=True)
-
-    def get_start_cmd(self):
-        cmd = [
-            "python",
-            "-m",
-            __name__.split(".")[0],
-            #"--host", self.host.netloc,
-            "--prefix", self.controller_prefix,
-            "--server", self.get_server_classpath(),
-        ]
-
-        if self.host:
-            cmd.extend(["--host", self.host])
-
-        config_path = self.config_path
-        if config_path:
-            cmd.extend(['--file', Path(config_path)])
-
-        return cmd
-
-    def get_server_classpath(self):
-        raise NotImplementedError()
-
-    def get_subprocess_args_and_kwargs(self):
-        args = [self.get_start_cmd()]
-        kwargs = dict(
-            stdout=subprocess.PIPE,
-            stderr=subprocess.STDOUT,
-            cwd=self.cwd,
-            env=self.environ
-        )
-        return args, kwargs
-
-    def start(self, **kwargs):
-        #import pdb; pdb.set_trace()
-        args, kwargs = self.get_subprocess_args_and_kwargs()
-        self.process = subprocess.Popen(*args, **kwargs)
-
-        self.quiet = kwargs.pop("quiet", type(self).quiet)
-        self.buf = deque(maxlen=self.bufsize)
-
-        self.thread = ServerThread(self)
-        self.thread.start()
-
-        self.host = Host(self.find_host()).client()
-
-    def find_host(self):
-        host = ""
-        attempts = 0
-        i = 0
-        while not host:
-            try:
-                m = re.search(self.host_regex, self.buf[i], flags=re.I)
-                if m:
-                    host = m.group(1)
-                else:
-                    i += 1
-
-            except IndexError:
-                attempts += 1
-                if attempts > 10000000:
-                    raise RuntimeError(
-                        "Tried {} time(s) to find host, is something wrong?".format(attempts)
-                    )
-
-        return host
-
-
-    def stop(self):
-        process = None
-        try:
-            process = self.process
-            process.kill()
-
-        except AttributeError:
-            pass
-
-        finally:
-            self.kill()
-
-            if process:
-                try:
-                    process.stdout.close()
-                except IOError:
-                    pass
-
diff --git a/endpoints/interface/tornado/__init__.py b/endpoints/interface/tornado/__init__.py
deleted file mode 100644
index 4add43e..0000000
--- a/endpoints/interface/tornado/__init__.py
+++ /dev/null
@@ -1,233 +0,0 @@
-# -*- coding: utf-8 -*-
-""" Interface for Tornado webserver"""
-from __future__ import unicode_literals, division, print_function, absolute_import
-import json
-import logging
-import io
-
-import tornado.web
-import tornado.websocket
-import tornado.routing
-import tornado.ioloop
-import tornado.httpserver
-import tornado.wsgi
-import tornado.httputil
-
-from .. import BaseServer, BaseWebsocketServer, Payload
-from ...compat import *
-from ...reflection import Reflect
-from ...http import Url, Host
-from ...utils import String, ByteString, JSONEncoder, FileWrapper
-from ... import environ
-
-
-logger = logging.getLogger(__name__)
-
-
-class WebsocketHandler(tornado.websocket.WebSocketHandler):
-    def check_origin(self, origin):
-        return True
-
-    def open(self):
-        self.set_nodelay(True)
-
-        self.call = self.request.application.server.connect_websocket_call(raw_request=self.request)
-        req = self.call.request
-        logger.info("Websocket {} Connecting".format(req.uuid))
-        res = self.call.handle()
-        self.send(req, res)
-
-    def on_message(self, message):
-
-        c = self.request.application.server.create_websocket_call(self.call.request, message)
-        req = self.call.request
-        logger.debug("Websocket {} message".format(req.uuid))
-        res = c.handle()
-        self.send(req, res)
-
-    def on_close(self):
-        c = self.request.application.server.disconnect_websocket_call(self.call.request)
-        req = c.request
-        logger.info("Websocket {} Disconnecting".format(req.uuid))
-        res = c.handle()
-
-    def send(self, req, res):
-        for s in self.request.application.server.create_websocket_response_body(req, res):
-            self.write_message(s)
-
-
-class Handler(tornado.web.RequestHandler):
-    """All requests will go through this handler, specifically the handle method"""
-    def handle(self, *args, **kwargs):
-        """all the magic happens here, this will take the tornado request, create
-        an endpoints request and then create the call instance and let endpoints
-        handle the request and then pass the result back to tornado
-
-        all the http method class basically just wrap this method, but you could
-        override any of the http methods individually if you wanted, or you can
-        override this method if you want to have common functionality"""
-
-        # !!! if I create the call before right here (like say in Application.find_handler
-        # then the request body won't be populated, I have no idea why
-        # https://www.tornadoweb.org/en/stable/guide/structure.html#handling-request-input
-        c = self.request.application.server.create_call(self.request)
-
-        # just in case the endpoints code needs the tornado code for some reason
-        c.application = self.request.application
-        c.tornado = self
-
-        c.handle()
-        res = c.response
-
-        self.set_status(res.code)
-
-        for h in res.headers.items():
-            #pout.v(h, ByteString(h[0]), ByteString(h[1]))
-            #self.set_header(ByteString(h[0]), ByteString(h[1]))
-            self.set_header(h[0], h[1])
-
-        for s in self.request.application.server.create_response_body(res):
-            self.write(s)
-
-    def head(self, *args, **kwargs): return self.handle(*args, **kwargs)
-    def get(self, *args, **kwargs): return self.handle(*args, **kwargs)
-    def post(self, *args, **kwargs):
-        return self.handle(*args, **kwargs)
-    def delete(self, *args, **kwargs): return self.handle(*args, **kwargs)
-    def patch(self, *args, **kwargs): return self.handle(*args, **kwargs)
-    def put(self, *args, **kwargs): return self.handle(*args, **kwargs)
-    def options(self, *args, **kwargs): return self.handle(*args, **kwargs)
-
-
-class Application(tornado.web.Application):
-    """The tornado application instance handles the routing, we override it so we
-    can use endpoints's routing stuff and so tornado's handler can get access to
-    the endpoints's Server instance so endpoints can do its thing"""
-    def __init__(self, server, *args, **kwargs):
-        self.server = server
-        kwargs.setdefault("default_handler_class", self.server.tornado_handler_class)
-        super(Application, self).__init__(*args, **kwargs)
-
-    def find_handler(self, request, **kwargs):
-        """This injects self into the request instance so the actual handler instance
-        can get access to the Server instance to create endpoints compatible things
-        and handle the request"""
-        request.application = self
-        return super(Application, self).find_handler(request, **kwargs)
-
-
-class Server(BaseServer):
-    """This is the bridge class between tornado and endpoints, this makes tornado
-    compatible with all of endpoints's stuff
-
-    https://github.com/tornadoweb/tornado/blob/master/tornado/web.py
-    https://www.tornadoweb.org/en/stable/web.html
-    https://www.tornadoweb.org/en/stable/
-    https://www.tornadoweb.org/en/stable/httpserver.html
-    https://www.tornadoweb.org/en/stable/guide/structure.html
-    https://github.com/tornadoweb/tornado/blob/master/tornado/httpserver.py
-    https://github.com/tornadoweb/tornado/blob/master/tornado/tcpserver.py
-    https://www.tornadoweb.org/en/stable/guide/structure.html
-    """
-
-    tornado_handler_class = Handler
-
-    tornado_application_class = Application
-
-    @property
-    def hostloc(self):
-        server_address = []
-        for s in self.backend._sockets.values():
-            server_address = s.getsockname()
-            break
-        return ":".join(map(String, server_address))
-
-    def create_backend(self, **kwargs):
-        h = Host(kwargs.pop('host', environ.HOST))
-        #hostname, port = Url.split_hostname_from_port(kwargs.pop('host', environ.HOST))
-        #port = port if port else 0
-
-        app = self.tornado_application_class(self)
-        server = app.listen(h.port, h.hostname)
-        #server.start(0) # I'm not sure what this did but it messed up py2, py3 stayed the same
-        return server
-
-    def create_request(self, raw_request, **kwargs):
-        """convert the raw interface raw_request to a request that endpoints understands
-
-        https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPMessageDelegate
-        https://github.com/tornadoweb/tornado/blob/stable/tornado/httputil.py
-        """
-        r = self.request_class()
-        r.set_headers(raw_request.headers)
-
-        # this call actually modifies the raw request by popping headers
-        environ = tornado.wsgi.WSGIContainer.environ(raw_request)
-        r.environ.update(environ)
-
-        r.method = raw_request.method
-        r.path = raw_request.path
-        #r.query = raw_request.query
-        r.query_kwargs = Url.normalize_query_kwargs(raw_request.query_arguments)
-
-        self.create_request_body(r, raw_request, **kwargs)
-        r.raw_request = raw_request
-        return r
-
-    def create_request_body(self, request, raw_request, **kwargs):
-        body_args = []
-        body_kwargs = {}
-        body = None
-        if raw_request.body_arguments:
-            body_kwargs = Url.normalize_query_kwargs(raw_request.body_arguments)
-
-        if raw_request.files:
-            for k, vs in Url.normalize_query_kwargs(raw_request.files).items():
-                body_kwargs[k] = FileWrapper(
-                    io.BytesIO(vs["body"]),
-                    name=vs["filename"],
-                    filename=vs["filename"],
-                    type=vs["content_type"],
-                    raw=vs,
-                )
-
-        if raw_request.body:
-            # tornado won't un-jsonify stuff automatically, so if there aren't
-            # any body arguments there might still be something in body
-            body = io.BytesIO(raw_request.body)
-            if request.is_json():
-                body = request.create_body(body)
-                body_kwargs = body.kwargs
-                body_args = body.args
-
-        request.body_args = body_args
-        request.body_kwargs = body_kwargs
-        request.body = body
-        return request
-
-    def serve_forever(self):
-        server = self.backend
-        tornado.ioloop.IOLoop.current().start()
-
-    def serve_count(self, count):
-        """TODO there might be a way to make this work by using IOLoop.run_sync
-        but it's not worth figuring out right now
-        https://github.com/tornadoweb/tornado/blob/master/tornado/ioloop.py#L460
-        """
-        raise NotImplementedError()
-
-
-class WebsocketServer(BaseWebsocketServer, Server):
-    """
-    https://github.com/tornadoweb/tornado/blob/master/tornado/websocket.py
-    https://www.tornadoweb.org/en/stable/websocket.html
-    """
-    tornado_handler_class = WebsocketHandler
-
-    payload_class = Payload
-
-    def create_backend(self, **kwargs):
-        kwargs.setdefault("websocket_ping_interval", 60)
-        kwargs.setdefault("websocket_ping_timeout", 60)
-        return super(WebsocketServer, self).create_backend(**kwargs)
-
diff --git a/endpoints/interface/tornado/client.py b/endpoints/interface/tornado/client.py
deleted file mode 100644
index b7811ef..0000000
--- a/endpoints/interface/tornado/client.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from ...compat import *
-from ..client import WebServer as BaseWebServer
-
-
-class WebServer(BaseWebServer):
-    def get_server_classpath(self):
-        return ".".join(__name__.split(".")[:-1]) + ".Server"
-
-
-class WebsocketServer(WebServer):
-    def get_server_classpath(self):
-        return ".".join(__name__.split(".")[:-1]) + ".WebsocketServer"
-
-
diff --git a/endpoints/interface/uwsgi/__init__.py b/endpoints/interface/uwsgi/__init__.py
deleted file mode 100644
index 47a63b5..0000000
--- a/endpoints/interface/uwsgi/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-try:
-    import uwsgi
-except ImportError:
-    uwsgi = None
-
-from ...compat import *
-from ..wsgi import Application
-
diff --git a/endpoints/interface/uwsgi/client.py b/endpoints/interface/uwsgi/client.py
deleted file mode 100644
index 7be1491..0000000
--- a/endpoints/interface/uwsgi/client.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-import sys
-import subprocess
-
-from ...compat import *
-from ...utils import String, Path
-from ..client import WebServer as BaseWebServer
-
-
-class WebServer(BaseWebServer):
-
-    process_count = 1
-
-    host_regex = r"bound\s+to\s+TCP\s+address\s+(([^:]+):(\d+))"
-
-    def get_plugins(self):
-        vi = sys.version_info
-
-        bp = subprocess.check_output(["which", "uwsgi"]).strip()
-        plugins_dir = Path(bp).resolve().parent
-
-        return [
-            "--plugins-dir", plugins_dir,
-            "--autoload",
-            "--plugin", "python",
-            "--plugin", "python{}{}".format(vi.major, vi.minor),
-        ]
-
-    def get_start_cmd(self):
-        cmd = [
-            "uwsgi",
-        ]
-
-        cmd.extend(self.get_plugins())
-
-        cmd.extend([
-            "--need-app",
-            "--show-config",
-            "--plugin-list",
-            #"--master",
-            "--processes", str(self.process_count),
-            "--cpu-affinity", "1",
-            "--thunder-lock",
-            "--http-raw-body",
-            "--chdir", self.cwd,
-        ])
-
-        if "VIRTUAL_ENV" in os.environ:
-            cmd.extend(["--virtualenv", os.environ["VIRTUAL_ENV"]])
-
-        if self.host:
-            cmd.extend(["--http", self.host])
-        else:
-            cmd.extend(["--http-socket", "0.0.0.0:0"])
-
-        if self.config_path:
-            cmd.extend(['--wsgi-file', Path(self.config_path)])
-
-        else:
-            cmd.extend([
-                "--module", "{}:Application()".format(".".join(__name__.split(".")[0:-1])),
-            ])
-
-        return cmd
-
-
-class WebsocketServer(WebServer):
-
-    async_process_count = 50
-
-    def get_plugins(self):
-        ret = super(WebsocketServer, self).get_plugins()
-
-        vi = sys.version_info
-        ret.extend([
-            "--plugin", "gevent",
-            "--plugin", "gevent{}{}".format(vi.major, vi.minor),
-        ])
-
-        return ret
-
-    def get_start_cmd(self):
-        cmd = super(WebsocketServer, self).get_start_cmd()
-        cmd.extend([
-            "--http-websockets",
-            "--gevent", String(self.async_process_count),
-        ])
-        return cmd
-
diff --git a/endpoints/interface/uwsgi/gevent.py b/endpoints/interface/uwsgi/gevent.py
deleted file mode 100644
index e949844..0000000
--- a/endpoints/interface/uwsgi/gevent.py
+++ /dev/null
@@ -1,207 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-import logging
-
-logger = logging.getLogger(__name__)
-
-try:
-    import gevent.select
-    import gevent.monkey
-except ImportError:
-    logger.error("you need to install gevent to use {}".format(__name__))
-    raise
-else:
-    if not gevent.monkey.saved:
-        # https://github.com/gevent/gevent/blob/master/src/gevent/monkey.py
-        logger.warning("Running gevent.monkey.patch_all() since not run previously")
-        gevent.monkey.patch_all()
-
-from ...compat import *
-from ...http import Request as BaseRequest
-from ...utils import ByteString
-from .. import Payload, BaseWebsocketServer
-from . import Application, uwsgi
-from ...exception import CloseConnection
-
-
-logger = logging.getLogger(__name__)
-
-
-class Connection(object):
-    """Handle the full websocket connection lifecycle
-    When a websocket connection is fully established, this class is created and
-    ties the user to the file descriptors for the websocket and Redis pubsub
-    connections. It handles receiving and routing the messages that come in from
-    and to the user.
-    It's created in Application.application_websocket and is only used there, so look
-    at that method to see how this object is used
-
-    https://github.com/unbit/uwsgi/blob/master/tests/websockets_chat.py
-    """
-
-    def __init__(self, app):
-        uwsgi.websocket_handshake()
-
-        self.app = app
-        #self.request = request
-        #self.response = response
-        self.pid = os.getpid()
-
-        self.ws_fd = uwsgi.connection_fd()
-        self.descriptors = [self.ws_fd]
-
-    def is_websocket(self, fd):
-        return fd == self.ws_fd
-
-    def recv_payload(self, fd):
-        """receive a message from the user that will be routed to other users, 
-        in other words, the user sent a message from their client that the server
-        is receiving on the internets"""
-
-        if self.is_websocket(fd):
-            while True:
-                # loop over payload until all queued messages are exhausted, if
-                # we don't consume them all then they will be silently discarded
-                # https://github.com/unbit/uwsgi/issues/1241#issuecomment-241366419
-                payload = uwsgi.websocket_recv_nb()
-                if payload and payload != 'undefined':
-                    yield payload
-
-                else:
-                    break
-
-    def send_payload(self, payload):
-        """take all the messages received from a redis pubsub channel_name and send it
-        down the websocket to the user"""
-
-        uwsgi.websocket_send(payload)
-
-    def __iter__(self):
-        """just keep iterating through ready to read file descriptors for all eternity
-
-        :returns: payload, the raw payload received from a socket
-        """
-        while True:
-            # logger.debug("user {} waiting".format(user.pk))
-            ready = gevent.select.select(self.descriptors, [], [], 10)
-            if not ready[0]:
-                # send websocket ping on timeout
-                uwsgi.websocket_recv_nb()
-                continue
-
-            for fd in ready[0]:
-                for payload in self.recv_payload(fd):
-                    if payload:
-                        yield payload
-
-    def close(self):
-        pass
-
-    def __hash__(self):
-        return int(self.ws_fd)
-
-    def handle_connected(self, req, res):
-        """called right after a successful websocket connection"""
-        pass
-
-    def handle_disconnected(self, req, res):
-        """called right after a successful websocket disconnection"""
-        pass
-
-    def handle_called(self, req, res):
-        """called each time after a controller has handled the request"""
-        pass
-
-
-class WebsocketApplication(BaseWebsocketServer, Application):
-
-    connection_class = Connection
-
-    def is_http(self, environ):
-        upgrade_header = environ.get("HTTP_UPGRADE", "")
-        return not upgrade_header or (upgrade_header.lower() != "websocket")
-
-    def create_connection(self):
-        return self.connection_class(self)
-
-    def send(self, req, res, conn):
-        for s in self.create_websocket_response_body(req, res):
-            conn.send_payload(s)
-
-    def handle_websocket_response(self, environ):
-        conn = self.create_connection()
-        req = None
-        message_count = 0
-
-        try:
-            c = self.connect_websocket_call(raw_request=environ)
-            req = c.request
-            res = c.handle()
-            if res.code < 400:
-
-                logger.info("Websocket {} Connected to server".format(req.uuid))
-                conn.handle_connected(req, res)
-
-                # send down connect success so js clients can know both success or
-                # failure, turns out only sending down failure causes client code
-                # to be more complex
-                self.send(req, res, conn)
-
-                for raw_request in conn:
-                    c = self.create_websocket_call(req, raw_request)
-                    message_count += 1
-                    logger.debug("Websocket {} message {} received on server".format(
-                        c.request.uuid,
-                        message_count,
-                    ))
-                    res = c.handle()
-                    conn.handle_called(c.request, res)
-                    self.send(c.request, res, conn)
-
-            else:
-                # send down the connect results so javascript webclients can know
-                # why we are about to disconnect
-                logger.debug("Websocket {} Failed to connect to server".format(req.uuid))
-                self.send(req, res, conn)
-
-        except CloseConnection as e:
-            req = None
-
-        except IOError as e:
-            # user disconnected
-            # a user disconnecting is usually manifested by "IOError: unable to
-            # receive websocket message", this is entirely normal and nothing to
-            # be concerned about, it basically means the client closed the connection
-            #logger.exception(e)
-            logger.debug("Websocket {} client disconnected from server: {}".format(req.uuid, e))
-            #logger.debug("Websocket {} client disconnected from server: {}".format(req.uuid, e), exc_info=True)
-            #logger.debug(e, exc_info=True)
-
-        except Exception as e:
-            logger.exception(e)
-
-        finally:
-            if req:
-                logger.info("Websocket {} Disconnected from server after {} message(s)".format(
-                    req.uuid,
-                    message_count,
-                ))
-                c = self.disconnect_websocket_call(req)
-                c.quiet = True
-                c.handle()
-
-            conn.close()
-            conn.handle_disconnected(req, res)
-
-        return ''
-
-    def __call__(self, environ, start_response):
-        if self.is_http(environ):
-            res = self.handle_http_response(environ, start_response)
-
-        else:
-            res = self.handle_websocket_response(environ)
-
-        return res
-
diff --git a/endpoints/interface/wsgi.py b/endpoints/interface/wsgi.py
new file mode 100644
index 0000000..377a6b3
--- /dev/null
+++ b/endpoints/interface/wsgi.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+import asyncio
+
+from datatypes import (
+    Host,
+    ThreadingWSGIServer,
+)
+
+from ..compat import *
+from ..config import environ
+from .base import BaseApplication
+
+
+class Application(BaseApplication):
+    """The Application that a WSGI server needs
+
+    this extends Server just to make it easier on the end user, basically, all
+    you need to do to use this is in your wsgi-file, you can just do:
+
+        from endpoints.interface.wsgi import Application
+        application = Application()
+
+    and you're good to go
+    """
+    def __call__(self, environ, start_response):
+        """this is what will be called for each request that that WSGI server
+        handles"""
+        return asyncio.run(super().__call__(environ, start_response))
+
+    def normalize_call_kwargs(self, environ, start_response):
+        return {
+            "environ": environ,
+            "start_response": start_response,
+        }
+
+    async def handle_http(self, environ, start_response):
+        request = await self.create_request(environ)
+        response = await self.create_response()
+        await self.handle(request, response)
+
+        start_response(
+            '{} {}'.format(response.code, response.status),
+            list(response.headers.items())
+        )
+
+        # https://peps.python.org/pep-0530/
+        return [body async for body in self.get_response_body(response)]
+
+    async def create_request(self, raw_request, **kwargs):
+        r = self.request_class()
+        for k, v in raw_request.items():
+            if k.startswith('HTTP_'):
+                r.set_header(k[5:], v)
+
+            elif k in ['CONTENT_TYPE', 'CONTENT_LENGTH']:
+                r.set_header(k, v)
+
+        r.method = raw_request['REQUEST_METHOD']
+        r.path = raw_request['PATH_INFO']
+        r.query = raw_request['QUERY_STRING']
+        r.scheme = raw_request.get('wsgi.url_scheme', "http")
+        r.host = raw_request["HTTP_HOST"]
+
+        await self.set_request_body(
+            r,
+            raw_request.get('wsgi.input', None),
+        )
+
+        r.raw_request = raw_request
+        return r
+
+
+class Server(ThreadingWSGIServer):
+    """A simple python WSGI Server
+
+    you would normally only use this with the endpoints command, if you
+    want to use it outside of that, then look at that script for inspiration
+    """
+    application_class = Application
+
+    def __init__(self, server_address=None, **kwargs):
+
+        if not server_address:
+            server_address = Host(kwargs.pop('host', environ.HOST))
+
+        if "wsgifile" not in kwargs and "application" not in kwargs:
+            kwargs["application"] = self.application_class()
+
+        super().__init__(server_address, **kwargs)
+
diff --git a/endpoints/interface/wsgi/__init__.py b/endpoints/interface/wsgi/__init__.py
deleted file mode 100644
index 74c005c..0000000
--- a/endpoints/interface/wsgi/__init__.py
+++ /dev/null
@@ -1,152 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
-import cgi
-import json
-
-from ...compat import *
-from .. import BaseServer
-from ...http import Host
-from ...decorators import property
-from ...utils import ByteString, String
-from ... import environ
-
-
-class Application(BaseServer):
-    """The Application that a WSGI server needs
-
-    this extends Server just to make it easier on the end user, basically, all you
-    need to do to use this is in your wsgi-file, you can just do:
-
-        from endpoints.interface.wsgi import Application
-        application = Application()
-
-    and be good to go
-    """
-    def __call__(self, environ, start_response):
-        """this is what will be called for each request that that WSGI server handles"""
-        return self.handle_http_response(environ, start_response)
-
-    def handle_http_response(self, environ, start_response):
-        c = self.create_call(environ)
-        res = c.handle()
-
-        if is_py2:
-            start_response(
-                ByteString('{} {}'.format(res.code, res.status)).raw(),
-                list((ByteString(h[0]).raw(), ByteString(h[1]).raw()) for h in res.headers.items())
-            )
-
-        else:
-            start_response(
-                '{} {}'.format(res.code, res.status),
-                list(res.headers.items())
-            )
-
-        return self.create_response_body(res)
-
-    def create_request(self, raw_request, **kwargs):
-        """
-        create instance of request
-
-        raw_request -- the raw request object retrieved from a WSGI server
-        """
-        r = self.request_class()
-        for k, v in raw_request.items():
-            if k.startswith('HTTP_'):
-                r.set_header(k[5:], v)
-            else:
-                r.environ[k] = v
-
-        r.method = raw_request['REQUEST_METHOD']
-        r.path = raw_request['PATH_INFO']
-        r.query = raw_request['QUERY_STRING']
-
-        # handle headers not prefixed with http
-        for k in ['CONTENT_TYPE', 'CONTENT_LENGTH']:
-            v = r.environ.pop(k, None)
-            if v:
-                r.set_header(k, v)
-
-        self.create_request_body(r, raw_request, **kwargs)
-        r.raw_request = raw_request
-        return r
-
-    def create_request_body(self, request, raw_request, **kwargs):
-        body_args = []
-        body_kwargs = {}
-        body = None
-        if 'wsgi.input' in raw_request:
-            body = raw_request['wsgi.input']
-            body = request.create_body(raw_request['wsgi.input'])
-            body_kwargs = body.kwargs
-            body_args = body.args
-
-        request.body_args = body_args
-        request.body_kwargs = body_kwargs
-        request.body = body
-        return request
-
-    def create_backend(self, **kwargs):
-        raise NotImplementedError()
-
-    def handle_request(self):
-        raise NotImplementedError()
-
-    def serve_forever(self):
-        raise NotImplementedError()
-
-    def serve_count(self, count):
-        raise NotImplementedError()
-
-
-# http://stackoverflow.com/questions/20745352/creating-a-multithreaded-server
-class WSGIHTTPServer(socketserver.ThreadingMixIn, WSGIServer):
-#class WSGIHTTPServer(socketserver.ForkingMixIn, WSGIServer):
-    """This is here to make the standard wsgi server multithreaded"""
-    pass
-
-
-class Server(BaseServer):
-    """A simple python WSGI Server
-
-    you would normally only use this with the bin/wsgiserver.py script, if you
-    want to use it outside of that, then look at that script for inspiration
-    """
-    application_class = Application
-
-    backend_class = WSGIHTTPServer
-
-    @property
-    def hostloc(self):
-        return ":".join(map(String, self.backend.server_address))
-
-    @property(cached="_application")
-    def application(self):
-        """if no application has been set, then create it using application_class"""
-        app = self.application_class()
-        return app
-
-    @application.setter
-    def application(self, v):
-        """allow overriding of the application factory, this allows you to set
-        your own application callable that will be used to handle requests, see
-        bin/wsgiserver.py script as an example of usage"""
-        self._application = v
-        self.backend.set_app(v)
-
-    def create_backend(self, **kwargs):
-        server_address = Host(kwargs.pop('host', environ.HOST))
-        s = self.backend_class(server_address, WSGIRequestHandler, **kwargs)
-        s.set_app(self.application)
-        return s
-
-    def handle_request(self):
-        #self.prepare()
-        return self.backend.handle_request()
-
-    def serve_forever(self):
-        #self.prepare()
-        return self.backend.serve_forever()
-
diff --git a/endpoints/interface/wsgi/client.py b/endpoints/interface/wsgi/client.py
deleted file mode 100644
index 6101b0d..0000000
--- a/endpoints/interface/wsgi/client.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from ...compat import *
-from ..client import WebServer as BaseWebServer
-
-
-class WebServer(BaseWebServer):
-    def get_server_classpath(self):
-        return ".".join(__name__.split(".")[:-1]) + ".Server"
-
-
diff --git a/endpoints/reflection.py b/endpoints/reflection.py
deleted file mode 100644
index 940cf4b..0000000
--- a/endpoints/reflection.py
+++ /dev/null
@@ -1,758 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-import sys
-import importlib
-import inspect
-import re
-import fnmatch
-import ast
-import collections
-
-import pkgutil
-
-from .compat import *
-from .decorators import property, version, param
-from .utils import String
-
-
-class ReflectDecorator(object):
-    """The information of each individual decorator on a given ReflectMethod will
-    be wrapped in this class"""
-
-    @property(cached="_parents")
-    def parents(self):
-        ret = []
-        decor = self.decorator
-        if inspect.isclass(decor):
-            parents = inspect.getmro(decor)
-            ret = parents[1:]
-        return ret
-
-    def __init__(self, name, args, kwargs, decorator):
-        self.name = name
-        self.args = args
-        self.kwargs = kwargs
-        self.decorator = decorator
-
-    def contains(self, obj):
-        ret = obj == self.decorator
-        if not ret:
-            for parent in self.parents:
-                if parent == obj:
-                    ret = True
-                    break
-
-        return ret
-
-    def __contains__(self, obj):
-        return self.contains(obj)
-
-
-class ReflectMethod(object):
-    """Reflects a method on a class"""
-
-    @property(cached="_required_args")
-    def required_args(self):
-        """return how many *args are needed to call the method"""
-        ret = []
-        info = self.get_info()
-        for param_d in info.get("params", []):
-            if param_d.get("required", False):
-                ret.append(param_d["name"])
-
-        if ret:
-            # we need to go through the param decorators and check path args,
-            # because it's entirely possible we can make something required in
-            # the method definition but make it optional in the decorator
-            for name, param_d in self.params.items():
-                if not isinstance(name, int):
-                    names = []
-                    dest = param_d.get("dest", "")
-                    if dest:
-                        names.append(dest)
-                    names.append(name)
-                    names.extend(param_d.get("other_names", []))
-
-                    # first name that is found wins
-                    for n in names:
-                        try:
-                            ret.remove(n)
-                            break
-                        except ValueError:
-                            pass
-
-
-        ret.extend([None] * (max(0, len(self.params) - len(ret))))
-
-        # now we will remove any non required path args that are left
-        for name, param_d in self.params.items():
-            if isinstance(name, int):
-                #pout.v(name, param_d, ret)
-                # since name is an integer it's a path variable
-                if param_d.get("required", False):
-                    if ret[name] is None:
-                        ret[name] = name
-                else:
-                    if name < len(ret):
-                        ret[name] = None
-
-        #pout.v(info, self.params)
-        return list(filter(lambda x: x is not None, ret))
-
-    @property(cached="_name")
-    def name(self):
-        """return the method name (GET, POST)"""
-        return self.method_name
-
-    @property(cached="_decorators")
-    def decorators(self):
-        decorators = self.reflect_class.decorators
-        return decorators.get(self.method_name, [])
-
-    @property(cached="_desc")
-    def desc(self):
-        """return the description of this method"""
-        doc = None
-        def visit_FunctionDef(node):
-            """ https://docs.python.org/2/library/ast.html#ast.NodeVisitor.visit """
-            if node.name != self.method_name:
-                return
-
-            doc = ast.get_docstring(node)
-            raise StopIteration(doc if doc else "")
-
-        target = self.reflect_class.cls
-        try:
-            node_iter = ast.NodeVisitor()
-            node_iter.visit_FunctionDef = visit_FunctionDef
-            node_iter.visit(ast.parse(inspect.getsource(target)))
-
-        except StopIteration as e:
-            doc = String(e)
-
-        return doc or ""
-
-    def __init__(self, method_name, method, reflect_class):
-        self.method_name = method_name
-        self.method = method
-        self.reflect_class = reflect_class
-
-    def get_info(self):
-        info = self.reflect_class.get_info()
-        return info[self.name][self.method_name]
-
-    def has_positionals(self):
-        """return True if this method accepts *args"""
-        return self.get_info().get("positionals", False)
-
-    def has_keywords(self):
-        """return True if this method accepts **kwargs"""
-        return self.get_info().get("keywords", False)
-
-
-class ReflectHTTPMethod(ReflectMethod):
-    """This encompasses the http verbs like POST and GET"""
-    @property(cached="_version")
-    def version(self):
-        ret = ""
-        for decor in self.decorators:
-            if version in decor:
-                ret = list(filter(None, decor.args))[0]
-                break
-        return ret
-
-    @property
-    def headers(self):
-        headers = {}
-        version = self.version
-        if version:
-            headers['Accept'] = "{};version={}".format(self.reflect_class.cls.content_type, version)
-        return headers
-
-    @property(cached="_name")
-    def name(self):
-        """return the method name (GET, POST)"""
-        bits = self.method_name.split("_", 2)
-        return bits[0]
-
-    @property(cached="_params")
-    def params(self):
-        """return information about the params that the given http option takes"""
-        ret = {}
-        for rd in self.decorators:
-            args = rd.args
-            kwargs = rd.kwargs
-            if param in rd:
-                is_required =  kwargs.get('required', 'default' not in kwargs)
-                ret[args[0]] = {'required': is_required, 'other_names': args[1:], 'options': kwargs}
-
-        return ret
-
-#     def positionals_count(self):
-#         for p in self.params:
-
-
-
-class ReflectClass(object):
-    method_class = ReflectMethod
-
-    decorator_class = ReflectDecorator
-
-    @property(cached="_methods")
-    def methods(self):
-        """
-        return the supported http method options that this class supports
-        return what http method options this endpoint supports (eg, POST, GET)
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
-
-        :returns: dict, each http method (eg, GET, POST) will have a key with the value
-            being every method from the controller that can satisfy the http method
-        """
-        ret = collections.defaultdict(list)
-        info = self.get_info()
-        for http_method, methods in info.items():
-            for method_name, method_info in methods.items():
-                ret[http_method].append(self.method_class(
-                    method_name,
-                    method_info["method"],
-                    reflect_class=self
-                ))
-        return ret
-
-    @property(cached="_decorators")
-    def decorators(self):
-        """Get all the decorators of all the option methods in the class
-
-        http://stackoverflow.com/questions/5910703/ specifically, I used this
-        answer http://stackoverflow.com/a/9580006
-        """
-        ret = {}
-        info = self.get_info()
-        for http_method, methods in info.items():
-            for method, method_info in methods.items():
-                ret[method] = method_info.get("decorators", [])
-
-        return ret
-
-    @property
-    def class_name(self):
-        return self.cls.__name__
-
-    @property
-    def module_name(self):
-        return self.cls.__module__
-
-    @property
-    def classpath(self):
-        return "{}.{}".format(self.module_name, self.class_name)
-
-    @property(cached="_module")
-    def module(self):
-        return self.reflect_module.module
-        #return ReflectModule(self.module_name).module
-
-    @property(cached="_desc")
-    def desc(self):
-        """return the description of this endpoint"""
-        doc = inspect.getdoc(self.cls) or ""
-        return doc
-
-    def __init__(self, reflect_module, cls):
-        self.reflect_module = reflect_module
-        self.cls = cls
-        self._cache = {} # cache of get_info methods return values
-
-    def is_private(self):
-        """return True if this class is considered private"""
-        return self.class_name.startswith('_')
-
-    def method(self, method_name):
-        """Returns information about the method_name on this class
-
-        :param method_name: string, the name of the method
-        :returns: ReflectMethod, the reflection information about the method
-        """
-        info = self.get_info()
-        for http_method, methods in info.items():
-            if method_name in methods:
-                method_info = methods[method_name]
-                return self.method_class(method_name, method_info["method"], reflect_class=self)
-
-        raise ValueError("No {}.{} method".format(self.classpath, method_name))
-
-    def get_info(self):
-        """Get all the decorators of all the option methods in the class
-
-        http://stackoverflow.com/questions/5910703/ specifically, I used this
-        answer http://stackoverflow.com/a/9580006
-        """
-        cached = self._cache.get("info_cache", None)
-        if cached is not None: return cached
-
-        ret = collections.defaultdict(dict)
-        res = collections.defaultdict(list)
-        mmap = {}
-
-        def get_val(na, default=None):
-            """given an inspect type argument figure out the actual real python
-            value and return that
-
-            :param na: ast.expr instanct
-            :param default: sets the default value for na if it can't be resolved
-            :returns: type, the found value as a valid python type
-            """
-            ret = None
-            if isinstance(na, ast.Num):
-                repr_n = repr(na.n)
-                val = na.n
-                vtype = float if '.' in repr_n else int
-                ret = vtype(val)
-
-            elif isinstance(na, ast.Str):
-                ret = str(na.s)
-
-            elif isinstance(na, ast.Name):
-                # http://stackoverflow.com/questions/12700893/
-                ret = getattr(builtins, na.id, None)
-                if not ret:
-                    ret = na.id
-                    if ret == 'True':
-                        ret = True
-                    elif ret == 'False':
-                        ret = False
-
-            elif isinstance(na, ast.Dict):
-                if na.keys:
-                    ret = {get_val(na_[0]): get_val(na_[1]) for na_ in zip(na.keys, na.values)}
-                else:
-                    ret = {}
-
-            elif isinstance(na, (ast.List, ast.Tuple)):
-                if na.elts:
-                    ret = [get_val(na_) for na_ in na.elts]
-                else:
-                    ret = []
-
-                if isinstance(na, ast.Tuple):
-                    ret = tuple(ret)
-
-            else:
-                ret = default
-
-            return ret
-
-        def is_super(childnode, parentnode):
-            """returns true if child node has a super() call to parent node"""
-            ret = False
-            for n in childnode.body:
-                if not isinstance(n, ast.Expr): continue
-
-                try:
-                    func = n.value.func
-                    func_name = func.attr
-                    if func_name == parentnode.name:
-                        ret = isinstance(func.value, ast.Call)
-                        break
-
-                except AttributeError as e:
-                    ret = False
-
-            return ret
-
-        def visit_FunctionDef(node):
-            """as the code is parsed any found methods will call this function
-            https://docs.python.org/2/library/ast.html#ast.NodeVisitor.visit
-            """
-
-            # if there is a super call in the method body we want to add the
-            # decorators from that super call also
-            add_decs = True
-            if node.name in res:
-                add_decs = is_super(mmap[node.name], node)
-
-            mmap[node.name] = node
-
-            if add_decs:
-                for n in node.decorator_list:
-                    d = {}
-                    name = ''
-                    args = []
-                    kwargs = {}
-
-                    # is this a call like @decorator or like @decorator(...)
-                    if isinstance(n, ast.Call):
-                        name = n.func.attr if isinstance(n.func, ast.Attribute) else n.func.id
-                        for an in n.args:
-                            args.append(get_val(an))
-
-                        for an in n.keywords:
-                            kwargs[an.arg] = get_val(an.value)
-
-                    else:
-                        name = n.attr if isinstance(n, ast.Attribute) else n.id
-
-                    d = {
-                        "name": name,
-                        "args": args,
-                        "kwargs": kwargs
-                    }
-
-                    # get the actual decorator from either the module (imported)
-                    # or from the global builtins
-                    decor = None
-                    if self.reflect_module:
-                        m = self.reflect_module.module
-                        decor = getattr(m, name, None)
-
-                    if not decor:
-                        decor = getattr(builtins, name, None)
-
-                    if not decor:
-                        raise RuntimeError("Could not find {} decorator class".format(name))
-
-                    d["decorator"] = decor
-
-                    #res[node.name].append((name, args, kwargs))
-                    res[node.name].append(self.decorator_class(**d))
-
-        node_iter = ast.NodeVisitor()
-        node_iter.visit_FunctionDef = visit_FunctionDef
-        for target_cls in inspect.getmro(self.cls):
-            if target_cls == object: break
-            node_iter.visit(ast.parse(inspect.getsource(target_cls).strip()))
-
-        http_methods = self._get_methods_info()
-        for http_method, method_names in http_methods.items():
-            for method_name in method_names:
-                m = mmap[method_name]
-                d = {
-                    "decorators": res.get(method_name, []),
-                    "method": getattr(self.cls, method_name),
-                }
-
-                # does the method have *args?
-                d["positionals"] = True if m.args.vararg else False
-                # does the method have **kwargs?
-                d["keywords"] = True if m.args.kwarg else False
-
-                args = []
-                kwargs = {}
-                #pout.v(m.args)
-
-                # we build a mapping of the defaults to where they would sit in
-                # the actual argument string (the defaults list starts at 0 but
-                # would correspond to the arguments after the required
-                # arguments, so we need to compensate for that
-                defaults = [None] * (len(m.args.args) - len(m.args.defaults))
-                defaults.extend(m.args.defaults)
-
-                # if we ever just switch to py3 we can use inpsect.Parameter
-                # here https://docs.python.org/3/library/inspect.html#inspect.Parameter
-                d["params"] = []
-                for i in range(1, len(m.args.args)):
-                    an = m.args.args[i]
-                    dp = {
-                        "name": an.id if is_py2 else an.arg,
-                        "required": True,
-                    }
-
-                    dan = defaults[i]
-                    if dan:
-                        dp["required"] = False
-                        dp["default"] = get_val(dan)
-
-                    #pout.v(an)
-                    d["params"].append(dp)
-
-                    #args.append(get_val(an))
-# 
-#     #             for an in m.keywords:
-#     #                 kwargs[an.arg] = get_val(an.value)
-# 
-#                 pout.v(m.name, args, kwargs)
-
-                ret[http_method][method_name] = d
-
-#         pout.x()
-#         pout.v(mmap.keys())
-#         pout.x()
-#         pout.v(mmap["POST"].args)
-#         pout.v(getattr(self.controller_class, "POST"))
-        #pout.v(ret)
-        self._cache["info_cache"] = ret
-        return ret
-        #return res
-
-    def _get_methods_info(self):
-        """
-        return all the methods found in this class
-
-        :returns: dict, the key is the method name, the value is a list of the methods
-            named key, it returns a list for the value so ReflectController works
-        """
-        cached = self._cache.get("_get_methods_info", None)
-        if cached is not None: return cached
-
-        ret = collections.defaultdict(list)
-        for method_name, method in inspect.getmembers(self.cls):
-            ret[method_name].append(method)
-
-        self._cache["_get_methods_info"] = ret
-        return ret
-
-
-class ReflectController(ReflectClass):
-    """This will encompass an entire Controller and have information on all the http
-    methods (eg, GET and POST)"""
-
-    method_class = ReflectHTTPMethod
-
-    @property(cached="_bits")
-    def bits(self):
-        bits = self.module_name.replace(self.controller_prefix, '', 1).lower()
-        bits = list(filter(None, bits.split(".")))
-
-        class_name = self.class_name.lower()
-        if class_name != "default":
-            bits.append(class_name)
-
-        return bits
-
-    @property(cached="_uri")
-    def uri(self):
-        return "/" + "/".join(self.bits)
-
-    def __init__(self, reflect_module, controller_class, controller_prefix):
-        """reflect a controller
-
-        :param controller_prefix: the base controller prefix that this controller
-            class resides in, this is needed so the path can be figured out, basically
-            if the controller prefix is foo.controllers and this controller is located
-            in the module foo.controllers.bar.che then the module path can be worked
-            out to bar/che using controller_prefix
-        :param controller_class: type, the actual python class
-        """
-        self.controller_prefix = controller_prefix
-        super(ReflectController, self).__init__(reflect_module, controller_class)
-
-    def is_private(self):
-        """return True if this endpoint is considered private"""
-        return super(ReflectController, self).is_private() or getattr(self.controller_class, 'private', False)
-
-    def _get_methods_info(self):
-        """
-        return the supported http method options that this class supports
-        return what http method options this endpoint supports (eg, POST, GET)
-
-        http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
-
-        :returns: dict, each supported http method (eg, GET, POST) will have a key
-            with the value being the method names on the controller that handle
-            the http method
-        """
-        cached = self._cache.get("_get_methods_info", None)
-        if cached is not None: return cached
-
-        ret = collections.defaultdict(list)
-        method_regex = re.compile(r"^([A-Z][A-Z0-9]+)(_|$)")
-        controller_methods = inspect.getmembers(self.cls)
-        for controller_method_name, controller_method in controller_methods:
-            m = method_regex.match(controller_method_name)
-            if m:
-                http_name = m.group(1)
-                ret[http_name].append(controller_method_name)
-
-        self._cache["_get_methods_info"] = ret
-        return ret
-
-
-class ReflectModule(object):
-    """Introspect on a given module_name/modulepath (eg foo.bar.che)"""
-
-    class_class = ReflectClass
-
-    @property
-    def path(self):
-        return self.find_module_import_path()
-
-    @property
-    def module(self):
-        """return the actual python module found at self.module_name"""
-        return importlib.import_module(self.module_name)
-        #return self.get_module(self.module_name)
-
-    @property
-    def module_names(self):
-        """return all the module names that this module encompasses
-
-        :returns: set, a set of string module names
-        """
-        module = self.module
-        #module = self.get_module(self.module_name)
-        module_names = set([self.module_name])
-
-        if hasattr(module, "__path__"):
-            # path attr exists so this is a package
-            rp = ReflectPath(module.__path__[0])
-            module_names.update(rp.find_module_names(rp.path, self.module_name))
-
-        return module_names
-
-    def __init__(self, module_name):
-        """
-        :param module_name: the module path of the module to introspect
-        """
-        self.module_name = module_name
-
-    def __iter__(self):
-        """This will iterate through this module and all its submodules
-
-        :returns: a generator that yields ReflectModule instances
-        """
-        for module_name in self.module_names:
-            yield type(self)(module_name)
-
-    def classes(self):
-        """yields all the (class_name, class_type) that is found in only this
-        module (not submodules)
-
-        this filters our private classes (classes that start with _)
-
-        :returns: a generator of ReflectClass instances
-        """
-        module = self.module
-        classes = inspect.getmembers(module, inspect.isclass)
-        for class_name, class_type in classes:
-            rc = self.class_class(self, class_type)
-            #if class_name.startswith('_'): continue
-            if not rc.is_private():
-                yield rc
-
-#     def get_module(self, module_name):
-#         """load a module by name"""
-#         return importlib.import_module(module_name)
-
-    def find_module_path(self):
-        """find where the master module is located"""
-        master_modname = self.module_name.split(".", 1)[0]
-        master_module = sys.modules[master_modname]
-        #return os.path.dirname(os.path.realpath(os.path.join(inspect.getsourcefile(endpoints), "..")))
-        path = os.path.dirname(inspect.getsourcefile(master_module))
-        return path
-
-    def find_module_import_path(self):
-        """find and return the importable path for the module"""
-        module_path = self.find_module_path()
-        path = os.path.dirname(module_path)
-        return path
-        #path = os.path.dirname(os.path.realpath(os.path.join(module_path, "..")))
-        #return os.path.dirname(os.path.realpath(os.path.join(inspect.getsourcefile(endpoints), "..")))
-
-
-class ReflectPath(object):
-    """Introspection on a given path"""
-    @property
-    def module_names(self):
-        """return all the module names that this module encompasses
-
-        :returns: set, a set of string module names
-        """
-        return self.find_module_names(self.path, "")
-
-    def __init__(self, path):
-        """
-        :param path: string, the path to introspect
-        """
-        self.path = path
-
-    def modules(self):
-        """yields ReflectModule instances for each found module under path"""
-        for module_name in self.module_names:
-            yield ReflectModule(module_name)
-
-    def find_module_names(self, path, prefix=""):
-        """recursive method that will find all the modules of the given path
-
-        :param path: string, the path to scan for modules/submodules
-        :param prefix: string, if you want found modules to be prefixed with a certain
-            module path
-        :returns: set, a set of submodule names under path prefixed with prefix
-        """
-        module_names = set()
-#         if prefix:
-#             module_names = set([prefix])
-#         else:
-#             module_names = set()
-
-        # https://docs.python.org/2/library/pkgutil.html#pkgutil.iter_modules
-        for module_info in pkgutil.iter_modules([path]):
-            # we want to ignore any "private" modules
-            if module_info[1].startswith('_'): continue
-
-            if prefix:
-                module_prefix = ".".join([prefix, module_info[1]])
-            else:
-                module_prefix = module_info[1]
-
-            if module_info[2]:
-                # module is a package
-                module_names.add(module_prefix)
-                submodule_names = self.find_module_names(os.path.join(path, module_info[1]), module_prefix)
-                module_names.update(submodule_names)
-            else:
-                module_names.add(module_prefix)
-
-        return module_names
-
-    def find_modules(self, callback):
-        """find all the modules where callback returns true and yield those
-
-        :param callback: callable, callback(module) where module is a ReflectModule
-            instance, if callback returns True, this will yield the module
-        :returns: generator, yielding ReflectModule instances where callback(module)
-            returns True
-        """
-        for module in self.modules():
-            if callback(module):
-                yield module
-
-
-class Reflect(object):
-    """
-    Reflect the controllers to reveal information about what endpoints are live
-    """
-    controller_class = ReflectController
-
-    @property
-    def controllers(self):
-        # avoid circular dependencies
-        from .call import Controller
-
-        for controller_prefix in self.controller_prefixes:
-            for rm in ReflectModule(controller_prefix):
-                for rc in rm.classes():
-                    if not issubclass(rc.cls, Controller): continue
-                    if rc.cls == Controller: continue
-
-                    controller = self.create_controller(
-                        reflect_module=rm,
-                        controller_class=rc.cls,
-                        controller_prefix=controller_prefix,
-                    )
-
-                    # filter out controllers that can't handle any requests
-                    if controller.methods:
-                        yield controller
-
-    def __init__(self, controller_prefixes):
-        self.controller_prefixes = controller_prefixes
-
-    def create_controller(self, *args, **kwargs):
-        return self.controller_class(*args, **kwargs)
-
-    def __iter__(self):
-        return self.controllers
-
-
diff --git a/endpoints/utils.py b/endpoints/utils.py
index 2434b2c..4ba6177 100644
--- a/endpoints/utils.py
+++ b/endpoints/utils.py
@@ -1,13 +1,9 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
 import mimetypes
-import sys
-import base64
 import json
 import types
-import copy
-from io import IOBase, FileIO
+from functools import cmp_to_key
+
 
 from datatypes import (
     ByteString,
@@ -15,55 +11,83 @@
     Base64,
     Path,
     Deepcopy,
+    Url as BaseUrl,
 )
 
 from .compat import *
-from . import environ
-
-
-class FileWrapper(FileIO):
-    """Wraps a file descriptor.
 
-    Honestly, this exists because Python2 won't allow you to add properties to a
-    descriptor (because it doesn't extend object), this is currently used for any
-    uploaded files"""
-    def __init__(self, fp, name=None, **kwargs):
-        self.fp = fp
-        self.name = name
 
-        for k, v in kwargs.items():
-            setattr(self, k, v)
+class Url(BaseUrl):
+    """a url object on steroids, this is here to make it easy to manipulate urls
+    we try to map the supported fields to their urlparse equivalents, with some
+    additions
+
+    https://tools.ietf.org/html/rfc3986.html
+
+    given a url http://user:pass@foo.com:1000/bar/che?baz=boom#anchor
+    with a controller: Bar
+
+    .scheme = http
+    .netloc = user:pass@foo.com:1000
+    .hostloc = foo.com:1000
+    .hostname = foo.com
+    .host() = http://foo.com
+    .port = 1000
+    .base = http://user:pass@foo.com:1000/bar/che
+    .fragment = anchor
+    .anchor = fragment
+    .uri = /bar/che?baz=boom#anchor
+    .host(...) = http://foo.com/...
+    .base(...) = http://foo.com/bar/che/...
+    .controller(...) = http://foo.com/bar/...
+    """
+    class_path = ""
 
-    def close(self, *args, **kwargs):
-        return self.fp.close(*args, **kwargs)
+    module_path = ""
 
-    def seekable(self):
-        return self.fp.seekable()
+    def module(self, *paths, **query_kwargs):
+        """create a new Url instance using the module path as a base
 
-    def seek(self, *args, **kwargs):
-        return self.fp.seek(*args, **kwargs)
+        :param *paths: list, the paths to append to the module path
+        :param **query_kwargs: dict, any query string params to add
+        :returns: new Url instance
+        """
+        kwargs = self._normalize_params(*paths, **query_kwargs)
+        if self.module_path:
+            if "path" in kwargs:
+                paths = self.normalize_paths(self.module_path, kwargs["path"])
+                kwargs["path"] = "/".join(paths)
+            else:
+                kwargs["path"] = self.module_path
+        return self.create(self.root, **kwargs)
 
-    def read(self, *args, **kwargs):
-        return self.fp.read(*args, **kwargs)
+    def controller(self, *paths, **query_kwargs):
+        """create a new url object using the controller path as a base
 
-    def readall(self):
-        return self.fp.readall()
+        if you have a controller `foo.BarController` then this would create a new
+        Url instance with `host/foo/bar` as the base path, so any *paths will be
+        appended to `/foo/bar`
 
-    def tell(self, *args, **kwargs):
-        return self.fp.tell(*args, **kwargs)
+        :example:
+            # controller foo.Bar(Controller)
 
-    def readline(self, *args, **kwargs):
-        return self.fp.readline(*args, **kwargs)
+            print url # http://host.com/foo/bar/some_random_path
 
-    def readlines(self, *args, **kwargs):
-        return self.fp.readlines(*args, **kwargs)
+            print url.controller() # http://host.com/foo/bar
+            print url.controller("che", boom="bam") # http://host/foo/bar/che?boom=bam
 
-    def __iter__(self):
-        for line in self.fp:
-            yield line
-
-    def writable(self):
-        return False
+        :param *paths: list, the paths to append to the controller path
+        :param **query_kwargs: dict, any query string params to add
+        :returns: new Url instance
+        """
+        kwargs = self._normalize_params(*paths, **query_kwargs)
+        if self.class_path:
+            if "path" in kwargs:
+                paths = self.normalize_paths(self.class_path, kwargs["path"])
+                kwargs["path"] = "/".join(paths)
+            else:
+                kwargs["path"] = self.class_path
+        return self.create(self.root, **kwargs)
 
 
 class MimeType(object):
@@ -79,8 +103,8 @@ def find(cls, val):
     def find_type(cls, val):
         """return the mimetype from the given string value
 
-        if value is a path, then the extension will be found, if val is an extension then
-        that will be used to find the mimetype
+        if value is a path, then the extension will be found, if val is an
+        extension then that will be used to find the mimetype
         """
         mt = ""
         index = val.rfind(".")
@@ -135,8 +159,8 @@ def _split_media_type(self, media_type):
 
     def _sort(self, a, b):
         '''
-        sort the headers according to rfc 2616 so when __iter__ is called, the accept media types are
-        in order from most preferred to least preferred
+        sort the headers according to rfc 2616 so when __iter__ is called, the
+        accept media types are in order from most preferred to least preferred
         '''
         ret = 0
 
@@ -169,15 +193,11 @@ def _sort(self, a, b):
         return ret
 
     def __iter__(self):
-        if is_py2:
-            sorted_media_types = sorted(self.media_types, self._sort, reverse=True)
-        else:
-            from functools import cmp_to_key
-            sorted_media_types = sorted(
-                self.media_types,
-                key=cmp_to_key(self._sort),
-                reverse=True
-            )
+        sorted_media_types = sorted(
+            self.media_types,
+            key=cmp_to_key(self._sort),
+            reverse=True
+        )
         for x in sorted_media_types:
             yield x
 
@@ -243,5 +263,106 @@ def default(self, obj):
             return String(obj)
 
         else:
-            return json.JSONEncoder.default(self, obj)
+            #return json.JSONEncoder.default(self, obj)
+            return super().default(obj)
+
+
+class Status(String):
+    def __new__(cls, code, **kwargs):
+        if code < 1000:
+            status = cls.get_http_status(code)
+
+        else:
+            status = cls.get_websocket_status(code)
+
+        if not status:
+            status = "UNKNOWN"
+
+        instance = super().__new__(cls, status)
+        instance.code = code
+        return instance
+
+    @classmethod
+    def get_http_status(cls, code, **kwargs):
+        status = ""
+        status_tuple = BaseHTTPRequestHandler.responses.get(code)
+        if status_tuple:
+            status = status_tuple[0]
+
+        return status
+
+    @classmethod
+    def get_websocket_status(cls, code, **kwargs):
+        """Get the websocket status code
+
+        https://github.com/Luka967/websocket-close-codes
+        """
+        status = ""
+
+        codes = {
+            # Successful operation / regular socket shutdown
+            1000: "Close Normal",
+
+            # Client is leaving (browser tab closing)
+            1001: "Close Going Away",
+
+            # Endpoint received a malformed frame
+            1002: "Close Protocol Error",
+
+            # Endpoint received an unsupported frame (e.g. binary-only endpoint
+            # received text frame)
+            1003: "Close Unsupported",
+
+            1004: "Reserved",
+
+            # Expected close status, received none
+            1005: "Closed No Status",
+
+            # No close code frame has been receieved
+            1006: "Close Abnormal",
+
+            # Endpoint received inconsistent message (e.g. malformed UTF-8)
+            1007: "Unsupported Payload",
+
+            # Generic code used for situations other than 1003 and 1009
+            1008: "Policy Violation",
+
+            # Endpoint won't process large frame
+            1009: "Close Too Large",
+
+            # Client wanted an extension which server did not negotiate
+            1010: "Mandatory Extension",
+
+            # Internal server error while operating
+            1011: "Server Error",
+
+            # Server/service is restarting
+            1012: "Service Restart",
+
+            # Temporary server condition forced blocking client's request
+            1013: "Try Again Later",
+
+            # Server acting as gateway received an invalid response
+            1014: "Bad Gateway",
+
+            # Transport Layer Security handshake failure
+            1015: "TLS Handshake Fail",
+        }
+
+        if code in codes:
+            status = codes[code]
+
+        elif code >= 1016 and code <= 1999:
+            status = "Reserved For Later"
+
+        elif code >= 2000 and code <= 2999:
+            status = "Reserved For WebSocket Extensions"
+
+        elif code >= 3000 and code <= 3999:
+            status = "Registered First Come First Serve at IANA"
+
+        elif code >= 4000 and code <= 4999:
+            status = "Available For Applications"
+
+        return status
 
diff --git a/setup.py b/setup.py
index 09d6eb6..9d5ff00 100644
--- a/setup.py
+++ b/setup.py
@@ -35,16 +35,26 @@ def read(path):
 setup(
     version=version,
     description='Get an api up and running quickly',
-    keywords="wsgi wsgi-server api api-server server framework web-framework REST rest-api",
+    keywords=" ".join([
+        "asgi",
+        "wsgi",
+        "wsgi-server",
+        "api",
+        "api-server",
+        "server",
+        "framework",
+        "web-framework",
+        "REST",
+        "rest-api",
+    ]),
     author='Jay Marcyes',
     author_email='jay@marcyes.com',
     url='http://github.com/jaymon/{}'.format(name),
     license="MIT",
-    install_requires=['decorators', 'datatypes'],
-    tests_require=['testdata', 'requests'],
+    install_requires=['datatypes'],
+    tests_require=['testdata', 'requests', "daphne"],
     extras_require={
-        'websockets': ["websocket-client", "gevent"],
-        'tornado': ["tornado"],
+        'websockets': ["websocket-client"],
         'html': ['jinja2'],
     },
     classifiers=[ # https://pypi.python.org/pypi?:action=list_classifiers


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index d123cc5..6bf5993 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -4,84 +4,144 @@
 import os
 import sys
 import logging
+import asyncio
 
 import testdata
 
-from endpoints import environ
-from endpoints.interface import BaseServer
+from endpoints.config import environ
+from endpoints.interface.base import BaseApplication
+from endpoints.call import Request
 
 
-testdata.basic_logging()
-#testdata.basic_logging(format='[%(levelname).1s|%(asctime)s|%(filename)s:%(lineno)s] %(message)s')
-#logging.basicConfig(format="[%(levelname).1s] %(message)s", level=logging.DEBUG, stream=sys.stdout)
-#logger = logging.getLogger(__name__)
+testdata.basic_logging(
+    levels={
+        "datatypes": "WARNING",
+        "asyncio": "WARNING",
+    },
+#     format="|".join([
+#         '[%(levelname).1s',
+#         '%(asctime)s',
+#         '%(process)d.%(thread)d',
+#         '%(name)s', # logger name
+#         '%(pathname)s:%(lineno)s] %(message)s',
+#     ]),
+)
 
 
-class Server(BaseServer):
+class Server(object):
     """This is just a wrapper to get access to the Interface handling code"""
-    def __init__(self, controller_prefix="", contents=""):
-        if not controller_prefix:
-            controller_prefix = testdata.get_module_name()
+    @property
+    def controller_prefix(self):
+        return self.application.controller_prefixes[0]
 
-        super(Server, self).__init__(
-            controller_prefixes=[controller_prefix]
-        )
-
-        if isinstance(contents, dict):
-            d = {}
-            for k, v in contents.items():
-                if k:
-                    d[".".join([controller_prefix, k])] = v
-                else:
-                    d[controller_prefix] = v
-            self.controllers = testdata.create_modules(d)
+    def __init__(self, *args, **kwargs):
+        self.application = BaseApplication(*args, **kwargs)
 
-        else:
-            self.controller = testdata.create_module(controller_prefix, contents=contents)
-
-    def create_request(self, path):
-        req = self.request_class()
-        req.method = self.method.upper()
+    def create_request(self, path, method, **kwargs):
+        if not (req := kwargs.pop("request", None)):
+            req = asyncio.run(self.application.create_request(None))
+            req.method = method.upper()
+            req.path = path
 
-        version = self.kwargs.pop("version", None)
+        version = kwargs.pop("version", None)
         if version is not None:
             req.set_header('Accept', '*/*;version={}'.format(version))
 
-        d = dict(self.kwargs)
+        d = dict(kwargs)
         d.setdefault("host", "endpoints.fake")
         for k, v in d.items():
             setattr(req, k, v)
 
-        req.path = path
         return req
 
-    def handle(self, path, method="GET", **kwargs):
-        """This isn't technically needed but just makes it explicit you pass in the
-        path you want and this will translate that and handle the request
+    def start(self):
+        pass
+
+    def stop(self):
+        pass
+
+    def handle(self, path="", method="GET", **kwargs):
+        """This isn't technically needed but just makes it explicit you pass in
+        the path you want and this will translate that and handle the request
 
         :param path: string, full URI you are requesting (eg, /foo/bar)
         """
-        self.method = method
-        self.kwargs = kwargs
-        c = self.create_call(path)
-        c.handle()
-        return c.response
+        request = self.create_request(path, method, **kwargs)
+        response = asyncio.run(self.application.create_response())
+        asyncio.run(self.application.handle(request, response))
+        return response
 
     def post(self, path, body_kwargs, **kwargs):
-        return self.handle(path, method="POST", body_kwargs=body_kwargs, **kwargs)
+        return self.handle(
+            path,
+            method="POST",
+            body_kwargs=body_kwargs,
+            **kwargs
+        )
 
     def get(self, path, query_kwargs, **kwargs):
-        return self.handle(path, method="GET", query_kwargs=query_kwargs, **kwargs)
+        return self.handle(
+            path,
+            method="GET",
+            query_kwargs=query_kwargs,
+            **kwargs
+        )
 
-    def path(self, *args):
-        bits = [""]
-        pout.v(self.controller.name)
-        bits.append(self.controller.name)
-        bits.extend(args)
-        return "/".join(bits)
+    def find(self, path="", method="GET", **kwargs):
+        kwargs["request"] = self.create_request(path, method, **kwargs)
+        return asyncio.run(
+            self.application.find_controller_info(**kwargs)
+        )
 
 
 class TestCase(testdata.TestCase):
+    server = None
+
+    server_class = Server
+
+    application_class = BaseApplication
+
     def get_host(self):
         return environ.HOST
 
+    def create_server(self, contents="", config_contents="", **kwargs):
+        if contents:
+            tdm = self.create_controller_module(contents, **kwargs)
+            kwargs["cwd"] = tdm.basedir
+            kwargs["controller_prefix"] = tdm
+
+        kwargs["host"] = self.get_host()
+
+        if config_contents:
+            config_path = testdata.create_file(
+                data=config_contents,
+                ext=".py",
+            )
+            kwargs["config_path"] = config_path
+
+        server = self.server_class(**kwargs)
+        server.stop()
+        server.start()
+        self.server = server
+        return server
+
+    def create_controller_module(self, contents, **kwargs):
+        if isinstance(contents, dict):
+            controller_prefix = kwargs.get("controller_prefix", "")
+            if not controller_prefix:
+                controller_prefix = testdata.get_module_name()
+
+            basedir = testdata.create_modules({controller_prefix: contents})
+            controller_prefix = basedir.modpath(controller_prefix)
+
+        else:
+            controller_prefix = testdata.create_module(
+                data=contents,
+                modpath=kwargs.get("controller_prefix", "")
+            )
+
+        return controller_prefix
+
+    def create_application(self, *args, **kwargs):
+        return self.application_class(*args, **kwargs)
+
diff --git a/tests/call_test.py b/tests/call_test.py
index 9ae69f7..cd80177 100644
--- a/tests/call_test.py
+++ b/tests/call_test.py
@@ -1,21 +1,19 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-from . import TestCase, skipIf, SkipTest, Server
-import os
+import asyncio
 
-import testdata
+from requests.auth import _basic_auth_str
 
-import endpoints
+from endpoints.compat import *
 from endpoints.environ import *
 from endpoints.utils import ByteString
-from endpoints.http import Request, Response
-from endpoints.call import Controller, Router
-from endpoints.exception import CallError
+from endpoints.call import Controller, Request, Response
+
+from . import TestCase, testdata
 
 
 class ControllerTest(TestCase):
     def test_any_1(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller, param",
             "class Default(Controller):",
             "    def ANY(self):",
@@ -34,7 +32,7 @@ def test_any_1(self):
         self.assertEqual(404, res.code)
 
     def test_any_2(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller, param",
             "class Default(Controller):",
             "    def ANY(self, **kwargs):",
@@ -52,7 +50,7 @@ def test_any_2(self):
         self.assertEqual("post", res.body)
 
     def test_unsupported_method_404(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller, param",
             "class Default(Controller):",
             "    def POST(self, foo):",
@@ -70,8 +68,8 @@ def test_unsupported_method_404(self):
     def test_cors(self):
         class Cors(Controller):
             def __init__(self, *args, **kwargs):
-                super(Cors, self).__init__(*args, **kwargs)
-                self.handle_cors()
+                super().__init__(*args, **kwargs)
+                asyncio.run(self.handle_cors())
             def POST(self): pass
 
         res = Response()
@@ -82,23 +80,39 @@ def POST(self): pass
 
         req.set_header('Origin', 'http://example.com')
         c = Cors(req, res)
-        self.assertEqual(req.get_header('Origin'), c.response.get_header('Access-Control-Allow-Origin')) 
+        self.assertEqual(
+            req.get_header('Origin'),
+            c.response.get_header('Access-Control-Allow-Origin')
+        ) 
 
         req.set_header('Access-Control-Request-Method', 'POST')
         req.set_header('Access-Control-Request-Headers', 'xone, xtwo')
         c = Cors(req, res)
-        c.OPTIONS()
-        self.assertEqual(req.get_header('Origin'), c.response.get_header('Access-Control-Allow-Origin'))
-        self.assertEqual(req.get_header('Access-Control-Request-Method'), c.response.get_header('Access-Control-Allow-Methods')) 
-        self.assertEqual(req.get_header('Access-Control-Request-Headers'), c.response.get_header('Access-Control-Allow-Headers')) 
+        asyncio.run(c.OPTIONS())
+        self.assertEqual(
+            req.get_header('Origin'),
+            c.response.get_header('Access-Control-Allow-Origin')
+        )
+        self.assertEqual(
+            req.get_header('Access-Control-Request-Method'),
+            c.response.get_header('Access-Control-Allow-Methods')
+        ) 
+        self.assertEqual(
+            req.get_header('Access-Control-Request-Headers'),
+            c.response.get_header('Access-Control-Allow-Headers')
+        ) 
 
         c = Cors(req, res)
         c.POST()
-        self.assertEqual(req.get_header('Origin'), c.response.get_header('Access-Control-Allow-Origin')) 
+        self.assertEqual(
+            req.get_header('Origin'),
+            c.response.get_header('Access-Control-Allow-Origin')
+        ) 
 
     def test_bad_typeerror(self):
-        """There is a bug that is making the controller method is throw a 404 when it should throw a 500"""
-        c = Server(contents=[
+        """There is a bug that is making the controller method throw a 404 when
+        it should throw a 500"""
+        c = self.create_server([
             "from endpoints import Controller",
             "class Default(Controller):",
             "    def GET(self):",
@@ -107,7 +121,7 @@ def test_bad_typeerror(self):
         res = c.handle('/')
         self.assertEqual(500, res.code)
 
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "class Bogus(object):",
             "    def handle_controller(self, foo):",
@@ -122,544 +136,186 @@ def test_bad_typeerror(self):
         self.assertEqual(500, res.code)
 
 
-class RouterTest(TestCase):
-    def get_http_instances(self, path="", method="GET"):
-        req = Request()
-        req.method = method
-        req.path = path
-        res = Response()
-        return req, res
-
-    def test_multiple_controller_prefixes_1(self):
-        r = testdata.create_modules({
-            "foo": os.linesep.join([
-                "from endpoints import Controller",
-                "class Default(Controller): pass",
-            ]),
-            "bar": os.linesep.join([
-                "from endpoints import Controller",
-                "class User(Controller): pass",
-            ]),
+class RequestTest(TestCase):
+    def test_get_auth_scheme(self):
+        r = Request()
+        r.set_headers({
+            "Authorization": "Basic FOOBAR",
         })
-        r = Router(["foo", "bar"])
-
-        t = r.find(*self.get_http_instances("/user"))
-        self.assertTrue("bar", t["module_name"])
 
-        t = r.find(*self.get_http_instances("/che"))
-        self.assertTrue("foo", t["module_name"])
+        self.assertEqual("Basic", r.get_auth_scheme())
+        self.assertTrue(r.is_auth("basic"))
+        self.assertTrue(r.is_auth("Basic"))
+        self.assertFalse(r.is_auth("bearer"))
 
+        r.headers["Authorization"] = "BLAH_TOKEN"
+        self.assertEqual("", r.get_auth_scheme())
+        self.assertFalse(r.is_auth("basic"))
 
-    def test_routing(self):
-        """there was a bug that caused errors raised after the yield to return another
-        iteration of a body instead of raising them"""
-        controller_prefix = "routing1"
-        contents = [
-            "from endpoints import Controller",
-            "class Default(Controller):",
-            "    def GET(self): pass",
-            "",
-            "class Foo(Controller):",
-            "    def GET(self): pass",
-            "",
-            "class Bar(Controller):",
-            "    def GET(self): pass",
-        ]
-        testdata.create_module(controller_prefix, contents=contents)
-
-        r = Router([controller_prefix])
-        info = r.find(*self.get_http_instances())
-        self.assertEqual(info['module_name'], controller_prefix)
-        self.assertEqual(info['class_name'], "Default")
-
-        r = Router([controller_prefix])
-        info = r.find(*self.get_http_instances("/foo/che/baz"))
-        self.assertEqual(2, len(info['method_args']))
-        self.assertEqual(info['class_name'], "Foo")
-
-    def test_default_match_with_path(self):
-        """when the default controller is used, make sure it falls back to default class
-        name if the path bit fails to be a controller class name"""
-        controller_prefix = "nomodcontroller2"
-        c = Server(controller_prefix, {"nmcon": [
-            "from endpoints import Controller",
-            "class Default(Controller):",
-            "    def GET(self, *args, **kwargs):",
-            "        return args[0]"
-        ]})
-        res = c.handle('/nmcon/8')
-        self.assertEqual("8", res.body)
-
-    def test_no_match(self):
-        """make sure a controller module that imports a class with the same as
-        one of the query args doesen't get picked up as the controller class"""
-        controller_prefix = "nomodcontroller"
-        contents = {
-            "{}.nomod".format(controller_prefix): [
-                "class Nomodbar(object): pass",
-                ""
-            ],
-            controller_prefix: [
-                "from endpoints import Controller",
-                "from .nomod import Nomodbar",
-                "class Default(Controller):",
-                "    def GET(): pass",
-                ""
-            ]
-        }
-        m = testdata.create_modules(contents)
-
-        path = '/nomodbar' # same name as one of the non controller classes
-        r = Router([controller_prefix])
-        info = r.find(*self.get_http_instances(path))
-        self.assertEqual('Default', info['class_name'])
-        self.assertEqual('nomodcontroller', info['module_name'])
-        self.assertEqual('nomodbar', info['method_args'][0])
-
-    def test_import_error(self):
-
-        controller_prefix = "importerrorcontroller"
-        c = Server(controller_prefix, [
-            "from endpoints import Controller",
-            "from does_not_exist import FairyDust",
-            "class Default(Controller):",
-            "    def GET(): pass",
-            ""
-        ])
-        res = c.handle('/')
-        self.assertEqual(404, res.code)
+    def test_is_oauth(self):
+        username = "foo"
+        password = "bar"
+        r = Request()
 
-    def test_get_controller_info_default(self):
-        """I introduced a bug on 1-12-14 that caused default controllers to fail
-        to be found, this makes sure that bug is squashed"""
-        controller_prefix = "controller_info_default"
-        r = testdata.create_modules({
-            controller_prefix: os.linesep.join([
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(): pass",
-                ""
-            ])
+        r.set_headers({"Authorization": _basic_auth_str(username, password)})
+        self.assertTrue(r.is_oauth("basic"))
+        self.assertTrue(r.is_oauth("client"))
+        self.assertFalse(r.is_oauth("token"))
+        self.assertFalse(r.is_oauth("access"))
+
+        r.headers["Authorization"] = "Bearer FOOBAR"
+        self.assertFalse(r.is_oauth("basic"))
+        self.assertFalse(r.is_oauth("client"))
+        self.assertTrue(r.is_oauth("token"))
+        self.assertTrue(r.is_oauth("access"))
+
+        r.headers.pop("Authorization")
+        self.assertFalse(r.is_oauth("basic"))
+        self.assertFalse(r.is_oauth("client"))
+        self.assertFalse(r.is_oauth("token"))
+        self.assertFalse(r.is_oauth("access"))
+
+    def test_copy(self):
+        r = Request()
+        r.set_headers({
+            "Host": "localhost",
         })
-
-        r = Router([controller_prefix])
-        info = r.find(*self.get_http_instances("/"))
-        self.assertEqual('Default', info['class_name'])
-        self.assertTrue(issubclass(info['class'], Controller))
-
-    def test_callback_info(self):
-        controller_prefix = "callback_info"
-        req, res = self.get_http_instances("/foo/bar")
-        req.query_kwargs = {'foo': 'bar', 'che': 'baz'}
-
-        r = Router([controller_prefix])
-
-        with self.assertRaises(ImportError):
-            d = r.find(req, res)
-
-        contents = [
-            "from endpoints import Controller",
-            "class Bar(Controller):",
-            "    def GET(*args, **kwargs): pass"
-        ]
-        testdata.create_module("{}.foo".format(controller_prefix), contents=contents)
-
-        # if it succeeds, then it passed the test :)
-        d = r.find(req, res)
-
-    def test_get_controller_info(self):
-        controller_prefix = "controller_info_advanced"
-        r = testdata.create_modules({
-            controller_prefix: [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.default".format(controller_prefix): [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.foo".format(controller_prefix): [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                "",
-                "class Bar(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                "    def POST(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.foo.baz".format(controller_prefix): [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                "",
-                "class Che(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.foo.boom".format(controller_prefix): [
-                "from endpoints import Controller",
-                "",
-                "class Bang(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
+        r.query = "foo=bar"
+        r.path = "/baz/che"
+        r.environ['wsgi.url_scheme'] = "http"
+        r.environ['SERVER_PORT'] = "80"
+        r.foo = 1
+
+        r2 = r.copy()
+        self.assertEqual(r.foo, r2.foo)
+        self.assertEqual(r.environ["SERVER_PORT"], r2.environ["SERVER_PORT"])
+
+        r2.headers["foo"] = "bar"
+        self.assertFalse("foo" in r.headers)
+        self.assertTrue("foo" in r2.headers)
+
+    def test_url(self):
+        """make sure the .url attribute is correctly populated"""
+        # this is wsgi configuration
+        r = Request()
+        r.set_headers({
+            "Host": "localhost",
         })
+        r.query = "foo=bar"
+        r.path = "/baz/che"
+        r.environ['wsgi.url_scheme'] = "http"
+        r.environ['SERVER_PORT'] = "80"
+        u = r.url
+        self.assertEqual("http://localhost/baz/che?foo=bar", r.url)
+        r.port = 555
+        u = r.url
+        self.assertEqual("http://localhost:555/baz/che?foo=bar", r.url)
+
+        # handle proxied connections
+        r.host = "localhost:10000"
+        r.port = "9000"
+        u = r.url
+        self.assertTrue(":10000" in u)
+
+    def test_charset(self):
+        r = Request()
+        r.set_header("content-type", "application/json;charset=UTF-8")
+        charset = r.encoding
+        self.assertEqual("UTF-8", charset)
 
-        ts = [
-            {
-                'in': dict(method="GET", path="/foo/bar/happy/sad"),
-                'out': {
-                    'module_name': "controller_info_advanced.foo",
-                    'class_name': 'Bar',
-                    'method_args': ['happy', 'sad'],
-                    #'method_name': "GET",
-                }
-            },
-            {
-                'in': dict(method="GET", path="/"),
-                'out': {
-                    'module_name': "controller_info_advanced",
-                    'class_name': 'Default',
-                    'method_args': [],
-                    #'method_name': "GET",
-                }
-            },
-            {
-                'in': dict(method="GET", path="/happy"),
-                'out': {
-                    'module_name': "controller_info_advanced",
-                    'class_name': 'Default',
-                    'method_args': ["happy"],
-                    #'method_name': "GET",
-                }
-            },
-            {
-                'in': dict(method="GET", path="/foo/baz"),
-                'out': {
-                    'module_name': "controller_info_advanced.foo.baz",
-                    'class_name': 'Default',
-                    'method_args': [],
-                    #'method_name': "GET",
-                }
-            },
-            {
-                'in': dict(method="GET", path="/foo/baz/che"),
-                'out': {
-                    'module_name': "controller_info_advanced.foo.baz",
-                    'class_name': 'Che',
-                    'method_args': [],
-                    #'method_name': "GET",
-                }
-            },
-            {
-                'in': dict(method="GET", path="/foo/baz/happy"),
-                'out': {
-                    'module_name': "controller_info_advanced.foo.baz",
-                    'class_name': 'Default',
-                    'method_args': ["happy"],
-                    #'method_name': "GET",
-                }
-            },
-            {
-                'in': dict(method="GET", path="/foo/happy"),
-                'out': {
-                    'module_name': "controller_info_advanced.foo",
-                    'class_name': 'Default',
-                    'method_args': ["happy"],
-                    #'method_name': "GET",
-                }
-            },
-        ]
-
-        for t in ts:
-            req, res = self.get_http_instances(**t['in'])
-
-            r = Router([controller_prefix])
-            d = r.find(req, res)
-            for key, val in t['out'].items():
-                self.assertEqual(val, d[key])
-
-
-class CallTest(TestCase):
-    def test_routing_error_unexpected_args(self):
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, foo):",
-            "        pass",
-        ])
-        res = c.handle("/foo/bar", query_kwargs=dict(foo=2))
-        self.assertEqual(404, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, foo):",
-            "        pass",
-        ])
-        res = c.handle("/foo/bar", query_kwargs=dict(che=2))
-        self.assertEqual(404, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, foo):",
-            "        pass",
-        ])
-        res = c.handle("/foo/bar", query_kwargs=dict(foo=1, bar=2))
-        self.assertEqual(404, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, foo):",
-            "        pass",
-        ])
-        res = c.handle("/foo", query_kwargs=dict(foo=1, bar=2))
-        self.assertEqual(409, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, foo):",
-            "        pass",
-        ])
-        res = c.handle("/foo", query_kwargs=dict(bar=2))
-        self.assertEqual(405, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, foo):",
-            "        pass",
-        ])
-        res = c.handle("/foo/bar")
-        self.assertEqual(404, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, foo, **kwargs):",
-            "        pass",
-        ])
-        res = c.handle("/foo/bar", query_kwargs=dict(foo=1, bar=2))
-        self.assertEqual(404, res.code)
-
-    def test_routing_error_no_args(self):
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self):",
-            "        pass",
-        ])
-        res = c.handle("/foo/bar/che/baz/boom/bam/blah")
-        self.assertEqual(404, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, **kwargs):",
-            "        pass",
-        ])
-        res = c.handle("/foo/bar/che/baz/boom/bam/blah", query_kwargs=dict(foo=1, bar=2))
-        self.assertEqual(404, res.code)
-
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self, **kwargs):",
-            "        return kwargs",
-        ])
-        res = c.handle("/", query_kwargs=dict(foo=1, bar=2))
-        self.assertEqual(200, res.code)
-        self.assertTrue("foo" in res._body)
+        r = Request()
+        r.set_header("content-type", "application/json")
+        charset = r.encoding
+        self.assertEqual(None, charset)
 
-        c = Server(contents=[
-            "from endpoints import Controller, param",
-            "class Default(Controller):",
-            "    def GET(self):",
-            "        return kwargs",
-        ])
-        res = c.handle("/", query_kwargs=dict(foo=1, bar=2))
-        self.assertEqual(405, res.code)
+    def test_ip(self):
+        r = Request()
+        r.set_header('REMOTE_ADDR', '172.252.0.1')
+        self.assertEqual('172.252.0.1', r.ip)
 
-    def test_lowercase_method(self):
-        c = Server("controller2", {"foo2": [
-            "from endpoints import Controller",
-            "class Bar(Controller):",
-            "    def get(*args, **kwargs): pass"
-        ]})
+        r = Request()
+        r.set_header('REMOTE_ADDR', '1.241.34.107')
+        self.assertEqual('1.241.34.107', r.ip)
 
-        res = c.handle("/foo2/bar", query_kwargs={'foo2': 'bar', 'che': 'baz'})
-        self.assertEqual(501, res.code)
+        r = Request()
+        r.set_header('x-forwarded-for', '54.241.34.107')
+        self.assertEqual('54.241.34.107', r.ip)
 
-    def test_handle_redirect(self):
-        c = Server("controllerhr", {"handle": [
-            "from endpoints import Controller, Redirect",
-            "class Testredirect(Controller):",
-            "    def GET(*args, **kwargs):",
-            "        raise Redirect('http://example.com')"
-        ]})
-
-        res = c.handle("/handle/testredirect")
-        self.assertEqual(302, res.code)
-        self.assertEqual('http://example.com', res.headers['Location'])
-
-    def test_handle_404_typeerror_1(self):
-        """make sure not having a controller is correctly identified as a 404"""
-        c = Server(contents=[
-            "from endpoints import Controller, Redirect",
-            "class NoFoo(Controller):",
-            "    def GET(*args, **kwargs):",
-            "        pass",
-        ])
+        r.set_header('x-forwarded-for', '127.0.0.1, 54.241.34.107')
+        self.assertEqual('54.241.34.107', r.ip)
 
-        res = c.handle("/foo/boom")
-        self.assertEqual(404, res.code)
+        r.set_header('x-forwarded-for', '127.0.0.1')
+        r.set_header('client-ip', '54.241.34.107')
+        self.assertEqual('54.241.34.107', r.ip)
 
-    def test_handle_405_typeerror_2(self):
-        """make sure 405 works when a path bit is missing"""
-        controller_prefix = "h404te2"
-        c = Server(controller_prefix, [
-            "from endpoints import Controller, decorators",
-            "class Default(Controller):",
-            "    def GET(self, needed_bit, **kwargs):",
-            "       return ''",
-            "",
-            "    def POST(self, needed_bit, **kwargs):",
-            "       return ''",
-            "",
-            "class Htype(Controller):",
-            "    def POST(self, needed_bit, **kwargs):",
-            "       return ''",
-            "",
-            "class Hdec(Controller):",
-            "    @decorators.param('foo', default='bar')",
-            "    def POST(self, needed_bit, **kwargs):",
-            "       return ''",
-            "",
-        ])
+    def test_ip_bad(self):
+        r = Request()
+        r.set_header('REMOTE_ADDR', "10.0.2.2")
+        r.set_header("Via", "1.1 ironport1.orlando.cit:80 (Cisco-WSA/9.0.1-162)")
+        self.assertEqual("", r.ip)
 
-        res = c.handle("/hdec", "POST")
-        self.assertEqual(405, res.code)
+        r = Request()
+        r.set_header('REMOTE_ADDR', "54.241.34.107")
+        r.set_header("Via", "1.1 ironport1.orlando.cit:80 (Cisco-WSA/9.0.1-162)")
+        self.assertEqual("54.241.34.107", r.ip)
 
-        res = c.handle("/htype", "POST")
-        self.assertEqual(405, res.code)
+    def test_properties(self):
 
-        res = c.handle("/")
-        self.assertEqual(405, res.code)
+        path = '/foo/bar'
+        path_args = ['foo', 'bar']
 
-        res = c.handle("/", "POST")
-        self.assertEqual(405, res.code)
+        r = Request()
+        r.path = path
+        self.assertEqual(r.path, path)
+        self.assertEqual(r.path_args, path_args)
 
-    def test_handle_404_typeerror_3(self):
-        """there was an error when there was only one expected argument, turns out
-        the call was checking for "arguments" when the message just had "argument" """
-        c = Server(contents=[
-            "from endpoints import Controller",
-            "class Foo(Controller):",
-            "    def GET(self): pass",
-            "",
-        ])
+        r = Request()
+        r.path_args = path_args
+        self.assertEqual(r.path, path)
+        self.assertEqual(r.path_args, path_args)
 
-        res = c.handle("/foo/bar/baz", query='che=1&boo=2')
-        self.assertEqual(404, res.code)
+        query = "foo=bar&che=baz&foo=che"
+        query_kwargs = {'foo': ['bar', 'che'], 'che': 'baz'}
 
-    def test_handle_accessdenied(self):
-        """raising an AccessDenied error should set code to 401 and the correct header"""
-        controller_prefix = "haccessdenied"
-        c = Server(controller_prefix, [
-            "from endpoints import Controller, AccessDenied",
-            "class Default(Controller):",
-            "    def GET(*args, **kwargs):",
-            "        raise AccessDenied(scheme='basic')",
-            "class Bar(Controller):",
-            "    def GET(*args, **kwargs):",
-            "        raise AccessDenied()",
-        ])
+        r = Request()
+        r.query = query
+        self.assertEqual(parse.parse_qs(r.query, True), parse.parse_qs(query, True))
+        self.assertEqual(r.query_kwargs, query_kwargs)
 
-        res = c.handle("/")
-        self.assertEqual(401, res.code)
-        self.assertTrue('Basic' in res.headers['WWW-Authenticate'])
-
-        res = c.handle("/bar")
-        self.assertEqual(401, res.code)
-        self.assertTrue('Auth' in res.headers['WWW-Authenticate'])
-
-    def test_handle_callstop(self):
-        controller_prefix = "handlecallstop"
-        c = Server(controller_prefix, [
-            "from endpoints import Controller, CallStop",
-            "class Testcallstop(Controller):",
-            "    def GET(*args, **kwargs):",
-            "        raise CallStop(205, None)",
-            "",
-            "class Testcallstop2(Controller):",
-            "    def GET(*args, **kwargs):",
-            "        raise CallStop(200, 'this is the body')",
-            "",
-            "class Testcallstop3(Controller):",
-            "    def GET(*args, **kwargs):",
-            "        raise CallStop(204, 'this is ignored')",
-        ])
+        r = Request()
+        r.query_kwargs = query_kwargs
+        self.assertEqual(parse.parse_qs(r.query, True), parse.parse_qs(query, True))
+        self.assertEqual(r.query_kwargs, query_kwargs)
 
-        res = c.handle("/testcallstop")
-        self.assertEqual(None, res.body)
-        self.assertEqual(None, res._body)
-        self.assertEqual(205, res.code)
+    def test_get_header(self):
+        r = Request()
 
-        res = c.handle("/testcallstop2")
-        self.assertEqual("this is the body", res.body)
-        self.assertEqual(200, res.code)
+        r.set_headers({
+            'foo': 'bar',
+            'Content-Type': 'application/json',
+            'Happy-days': 'are-here-again'
+        })
+        v = r.get_header('foo', 'che')
+        self.assertEqual('bar', v)
 
-        res = c.handle("/testcallstop3")
-        self.assertEqual(None, res._body)
-        self.assertEqual(204, res.code)
+        v = r.get_header('Foo', 'che')
+        self.assertEqual('bar', v)
 
-    def test_nice_error_405(self):
-        controller_prefix = "nicerr405"
-        c = Server(controller_prefix, [
-            "from endpoints import Controller",
-            "class Foo(Controller):",
-            "    def GET(self, bar): pass",
-            "",
-        ])
+        v = r.get_header('FOO', 'che')
+        self.assertEqual('bar', v)
 
-        # TODO -- capture stdout to make sure the error printed out, until then
-        # you will just have to manually check to make sure the warning was raised
-        # correctly
-        res = c.handle("/foo/bar/che")
+        v = r.get_header('che', 'che')
+        self.assertEqual('che', v)
 
-    def test_handle_method_chain(self):
-        controller_prefix = "handle_method_chain"
-        c = Server(controller_prefix, [
-            "from endpoints import Controller",
-            "class Foo(Controller):",
-            "    def handle_GET(self, *args, **kwargs):",
-            "        self.response.handle_GET_called = True",
-            "        self.handle(*args, **kwargs)",
-            "",
-            "    def handle(self, *args, **kwargs):",
-            "        self.response.handle_called = True",
-            "        super(Foo, self).handle(*args, **kwargs)",
-            "",
-            "    def GET(self, *args, **kwargs):",
-            "        self.response.GET_called = True",
-            "",
-        ])
+        v = r.get_header('che')
+        self.assertEqual(None, v)
 
-        res = c.handle("/foo")
-        self.assertTrue(res.handle_GET_called)
-        self.assertTrue(res.handle_called)
-        self.assertTrue(res.GET_called)
+        v = r.get_header('content-type')
+        self.assertEqual('application/json', v)
 
+        v = r.get_header('happy-days')
+        self.assertEqual('are-here-again', v)
 
-class CallVersioningTest(TestCase):
     def test_get_version(self):
         r = Request()
         r.set_header('accept', 'application/json;version=v1')
@@ -674,7 +330,8 @@ def test_get_version(self):
         self.assertEqual("", v)
 
     def test_get_version_default(self):
-        """turns out, calls were failing if there was no accept header even if there were defaults set"""
+        """turns out, calls were failing if there was no accept header even if
+        there were defaults set"""
         r = Request()
         r.headers = {}
         self.assertEqual("", r.version('application/json'))
@@ -691,3 +348,76 @@ def test_get_version_default(self):
         r.set_header('accept', '*/*;version=v8')
         self.assertEqual('v8', r.version('application/json'))
 
+
+class ResponseTest(TestCase):
+    def test_headers(self):
+        """make sure headers don't persist between class instantiations"""
+        r = Response()
+        r.headers["foo"] = "bar"
+        self.assertEqual("bar", r.headers["foo"])
+        self.assertEqual(1, len(r.headers))
+
+        r = Response()
+        self.assertFalse("foo" in r.headers)
+        self.assertEqual(0, len(r.headers))
+
+    def test_status(self):
+        r = Response()
+        for code, status in BaseHTTPRequestHandler.responses.items():
+            r.code = code
+            self.assertEqual(status[0], r.status)
+            r.code = None
+            r.status = None
+
+        r = Response()
+        r.code = 1000
+        self.assertEqual("UNKNOWN", r.status)
+
+    def test_body_file(self):
+        r = Response()
+        self.assertFalse("Content-Type" in r.headers)
+        self.assertFalse("Content-Length" in r.headers)
+
+        path = testdata.create_file("12345", ext="jpg")
+        with path.open() as fp:
+            r.body = fp
+            mt = r.headers["Content-Type"]
+            fs = r.headers["Content-Length"]
+            self.assertEqual("image/jpeg", mt)
+            self.assertEqual(5, int(fs))
+
+        path = testdata.create_file("123", ext="txt")
+        with path.open() as fp:
+            r.body = fp
+            mt = r.headers["Content-Type"]
+            fs = r.headers["Content-Length"]
+            self.assertEqual("text/plain", mt)
+            self.assertEqual(3, int(fs))
+
+    def test_code(self):
+        r = Response()
+        self.assertEqual(204, r.code)
+
+        r.body = "this is the body"
+        self.assertEqual(200, r.code)
+
+        r.code = 404
+        self.assertEqual(404, r.code)
+
+        r.body = "this is the body 2"
+        self.assertEqual(404, r.code)
+
+        r.body = None
+        self.assertEqual(404, r.code)
+
+        # now let's test defaults
+        del(r._code)
+
+        self.assertEqual(204, r.code)
+
+        r.body = ''
+        self.assertEqual(200, r.code)
+
+        r.body = {}
+        self.assertEqual(200, r.code)
+
diff --git a/tests/client_test.py b/tests/client_test.py
index 1faf596..6df2d09 100644
--- a/tests/client_test.py
+++ b/tests/client_test.py
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 
-from endpoints.client import WebClient, WebsocketClient
+from endpoints.client import HTTPClient, WebSocketClient
 from . import TestCase
 
 
-class WebClientTestCase(TestCase):
-    """Tests the HTTP webclient, this class has no server so it can only test the 
-    building block methods, I actual requesting of the client is tested in the interface"""
-    client_class = WebClient
+class HTTPClientTestCase(TestCase):
+    """Tests the HTTP webclient, this class has no server so it can only test
+    the building block methods, I actual requesting of the client is tested in
+    the interface"""
+    client_class = HTTPClient
 
     def create_client(self, **kwargs):
         kwargs.setdefault("host", "endpoints.fake")
@@ -35,8 +35,8 @@ def test_basic_auth(self):
         self.assertRegex(c.headers["authorization"], r"Basic\s+[a-zA-Z0-9=]+")
 
 
-class WebsocketClientTestCase(WebClientTestCase):
-    client_class = WebsocketClient
+class WebSocketClientTestCase(HTTPClientTestCase):
+    client_class = WebSocketClient
 
     def test_get_fetch_request(self):
         c = self.create_client()
@@ -48,3 +48,17 @@ def test_get_fetch_request(self):
         self.assertTrue('"foo": 1' in p)
         self.assertTrue('"bar": 2' in p)
 
+    def test_get_fetch_host(self):
+        client_cls = self.client_class
+        c = client_cls("http://localhost")
+        self.assertTrue(c.get_fetch_host().startswith("ws"))
+
+        c = client_cls("https://localhost")
+        self.assertTrue(c.get_fetch_host().startswith("wss"))
+
+        c = client_cls("HTTPS://localhost")
+        self.assertTrue(c.get_fetch_host().startswith("wss"))
+
+        c = client_cls("HTTP://localhost")
+        self.assertTrue(c.get_fetch_host().startswith("ws"))
+
diff --git a/tests/decorators_test.py b/tests/decorators_test.py
index 0e2afc5..87e10ba 100644
--- a/tests/decorators_test.py
+++ b/tests/decorators_test.py
@@ -1,20 +1,21 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-from . import TestCase, skipIf, SkipTest, Server
 import time
 import re
 
-import testdata
-
 import endpoints
-from endpoints import CallError
-from endpoints import decorators
-from endpoints.utils import ByteString, Base64, String
-from endpoints.http import Request
-from endpoints.decorators import (
-    param,
-    param_body,
-    param_query
+from endpoints.call import (
+    Controller,
+    Request,
+    Response,
+)
+from endpoints.exception import (
+    CallError,
+    AccessDenied,
+)
+from endpoints.utils import Base64, String
+from endpoints.decorators.base import (
+    ControllerDecorator,
+    BackendDecorator,
 )
 from endpoints.decorators.limit import (
     RateLimitDecorator,
@@ -23,28 +24,111 @@
     ratelimit_param,
     ratelimit_param_ip,
 )
+from endpoints.decorators.auth import (
+    AuthBackend,
+    AuthDecorator,
+    auth_basic,
+    auth_client,
+    auth_token,
+)
+from endpoints.decorators.utils import (
+    httpcache,
+    nohttpcache,
+    code_error,
+    param,
+    param_body,
+    param_query
+)
+
+from . import (
+    TestCase as BaseTestCase,
+    testdata,
+)
+
+
+class TestCase(BaseTestCase):
+    def create_controller(self):
+        class FakeController(Controller):
+            async def POST(self): pass
+            async def GET(self): pass
+
+        res = Response()
+        req = Request()
+        req.method = 'GET'
+
+        c = FakeController(req, res)
+        return c
+
+
+class ControllerDecoratorTest(TestCase):
+    def test_async_handle(self):
+        class Dec(ControllerDecorator):
+            async def handle(self, *args, **kwargs):
+                return True
+
+        c = self.create_controller()
+        @Dec()
+        async def afunc(self):
+            return 1
+        func = self.mock_async(afunc)
+
+        self.assertEqual(1, func(c))
+
+    def test_sync_handle(self):
+        class Dec(ControllerDecorator):
+            def handle(self, *args, **kwargs):
+                return True
 
+        c = self.create_controller()
+        @Dec()
+        async def afunc(self):
+            return 1
+        func = self.mock_async(afunc)
 
-def create_controller():
-    class FakeController(endpoints.Controller):
-        def POST(self): pass
-        def GET(self): pass
+        self.assertEqual(1, func(c))
 
-    res = endpoints.Response()
 
-    req = endpoints.Request()
-    req.method = 'GET'
+class BackendDecoratorTest(TestCase):
+    def test_async_handle(self):
+        class Backend(object):
+            async def handle(self, *args, **kwargs):
+                return True
+
+        class Dec(BackendDecorator):
+            backend_class = Backend
+
+        c = self.create_controller()
+        @Dec()
+        async def afunc(self):
+            return 1
+        func = self.mock_async(afunc)
 
-    c = FakeController(req, res)
-    return c
 
+        self.assertEqual(1, func(c))
+
+    def test_sync_handle(self):
+        class Backend(object):
+            def handle(self, *args, **kwargs):
+                return True
+
+        class Dec(BackendDecorator):
+            backend_class = Backend
 
-class RatelimitTest(TestCase):
+        c = self.create_controller()
+        @Dec()
+        async def afunc(self):
+            return 1
+        func = self.mock_async(afunc)
+
+        self.assertEqual(1, func(c))
+
+
+class RateLimitTest(TestCase):
     def set_bearer_auth_header(self, request, access_token):
         request.set_header("authorization", 'Bearer {}'.format(access_token))
 
     def test_throttle(self):
-        class TARA(object):
+        class MockObject(object):
             @ratelimit_ip(limit=3, ttl=1)
             def foo(self): return 1
 
@@ -52,10 +136,10 @@ def foo(self): return 1
             def bar(self): return 2
 
 
-        r_foo = endpoints.Request()
+        r_foo = Request()
         r_foo.set_header("X_FORWARDED_FOR", "276.0.0.1")
         r_foo.path = "/foo"
-        c = TARA()
+        c = self.mock_async(MockObject())
         c.request = r_foo
 
         for x in range(3):
@@ -94,14 +178,14 @@ class MockObject(object):
             @ratelimit_ip(limit=3, ttl=1)
             def foo(self): return 1
 
-        o = MockObject()
+        o = self.mock_async(MockObject())
         o.request = Request()
         o.request.set_header("X_FORWARDED_FOR", "100.1.1.1")
         o.request.path = "/fooip"
 
-        o.foo()
-        o.foo()
-        o.foo()
+        for _ in range(3):
+            o.foo()
+
         with self.assertRaises(CallError) as cm:
             o.foo()
         self.assertEqual(429, cm.exception.code)
@@ -124,7 +208,7 @@ class MockObject(object):
             @ratelimit_access_token(limit=2, ttl=1)
             def foo(self): return 1
 
-        o = MockObject()
+        o = self.mock_async(MockObject())
         o.request = Request()
         self.set_bearer_auth_header(o.request, "footoken")
         o.request.path = "/footoken"
@@ -153,18 +237,17 @@ def foo(self): return 1
         r = o.foo()
         self.assertEqual(1, r)
 
-    def test_ratelimit_param(self):
+    def test_ratelimit_param_only(self):
         class MockObject(object):
             @ratelimit_param("bar", limit=2, ttl=1)
             def foo(self, **kwargs): return 1
 
-        o = MockObject()
+        o = self.mock_async(MockObject())
         o.request = Request()
         self.set_bearer_auth_header(o.request, "footoken")
         o.request.path = "/fooparam"
 
         o.foo(bar="che")
-
         o.foo(bar="che")
 
         with self.assertRaises(CallError) as cm:
@@ -198,7 +281,7 @@ class MockObject(object):
             @ratelimit_param_ip("bar", limit=1, ttl=1)
             def foo(self, **kwargs): return 1
 
-        o = MockObject()
+        o = self.mock_async(MockObject())
         o.request = create_request("200.1.1.1")
         o.foo(bar="che")
 
@@ -245,7 +328,7 @@ def rl_param(self, **kwargs): return 4
                 @ratelimit_access_token()
                 def rl_access_token(self): return 5
 
-            o = MockObject()
+            o = self.mock_async(MockObject())
 
             with self.assertRaises(CallError):
                 o.rl_param_ip(bar=1)
@@ -259,8 +342,31 @@ def rl_access_token(self): return 5
             with self.assertRaises(CallError):
                 o.rl_access_token()
 
+    def test_async(self):
+        class MockObject(object):
+            request = Request()
+
+            @ratelimit_ip()
+            async def rl_ip(self): return 2
+
+            @ratelimit_param_ip("bar")
+            async def rl_param_ip(self, **kwargs): return 3
+
+            @ratelimit_param("bar")
+            async def rl_param(self, **kwargs): return 4
+
+            @ratelimit_access_token()
+            async def rl_access_token(self): return 5
 
-class AuthTest(TestCase):
+        o = self.mock_async(MockObject())
+
+        self.assertEqual(2, o.rl_ip())
+        self.assertEqual(3, o.rl_param_ip(bar=1))
+        self.assertEqual(4, o.rl_param(bar=1))
+        self.assertEqual(5, o.rl_access_token())
+
+
+class AuthDecoratorTest(TestCase):
     def get_basic_auth_header(self, username, password):
         credentials = Base64.encode('{}:{}'.format(username, password))
         return 'Basic {}'.format(credentials)
@@ -268,162 +374,174 @@ def get_basic_auth_header(self, username, password):
     def get_bearer_auth_header(self, access_token):
         return 'Bearer {}'.format(access_token)
 
-
     def test_bad_setup(self):
+        class Backend(AuthBackend):
+            async def handle(*args, **kwargs):
+                return False
 
-        def target(controller, *args, **kwargs):
-            return False
-
-        class TARA(object):
-            @decorators.auth_token(target=target)
+        class MockObject(object):
+            @auth_token(backend_class=Backend)
             def foo_token(self): pass
 
-            @decorators.auth_client(target=target)
-            def foo_client(self): pass
+            @auth_client(backend_class=Backend)
+            async def foo_client(self): pass
 
-            @decorators.auth_basic(target=target)
+            @auth_basic(backend_class=Backend)
             def foo_basic(self): pass
 
-            @decorators.auth(target=target)
-            def foo_auth(self): pass
-
-        r = endpoints.Request()
-        c = TARA()
-        c.request = r
+        c = self.mock_async(MockObject())
+        c.request = Request()
 
-        for m in ["foo_token", "foo_client", "foo_basic", "foo_auth"]: 
-            with self.assertRaises(endpoints.AccessDenied):
+        for m in ["foo_token", "foo_client", "foo_basic"]: 
+            with self.assertRaises(AccessDenied):
                 getattr(c, m)()
 
-    def test_token_auth(self):
-        def target(controller, access_token):
-            if access_token != "bar":
-                raise ValueError()
-            return True
+    def test_auth_token(self):
+        class Backend(AuthBackend):
+            async def auth_token(self, controller, access_token):
+                if access_token == "foo":
+                    raise ValueError()
 
-        def target_bad(controller, *args, **kwargs):
-            return False
+                if access_token == "bar":
+                    return True
 
-        class TARA(object):
-            @decorators.auth_token(target=target)
+                elif access_token == "che":
+                    return False
+
+        class MockObject(object):
+            @auth_token(backend_class=Backend)
             def foo(self): pass
 
-            @decorators.auth_token(target=target_bad)
-            def foo_bad(self): pass
+        r = Request()
 
-        r = endpoints.Request()
-        c = TARA()
+        c = self.mock_async(MockObject())
         c.request = r
 
         r.set_header('authorization', self.get_bearer_auth_header("foo"))
-        with self.assertRaises(endpoints.AccessDenied):
+        with self.assertRaises(AccessDenied):
             c.foo()
 
         r.set_header('authorization', self.get_bearer_auth_header("bar"))
         c.foo()
 
-        r = endpoints.Request()
+        r = Request()
         c.request = r
 
         r.body_kwargs["access_token"] = "foo"
-        with self.assertRaises(endpoints.AccessDenied):
+        with self.assertRaises(AccessDenied):
             c.foo()
 
         r.body_kwargs["access_token"] = "bar"
         c.foo()
 
-        r = endpoints.Request()
+        r = Request()
         c.request = r
 
         r.query_kwargs["access_token"] = "foo"
-        with self.assertRaises(endpoints.AccessDenied):
+        with self.assertRaises(AccessDenied):
             c.foo()
 
         r.query_kwargs["access_token"] = "bar"
         c.foo()
 
-        with self.assertRaises(endpoints.AccessDenied):
-            c.foo_bad()
-
-    def test_client_auth(self):
-        def target(controller, client_id, client_secret):
-            return client_id == "foo" and client_secret == "bar"
-
-        def target_bad(controller, *args, **kwargs):
-            return False
+        r.query_kwargs["access_token"] = "che"
+        with self.assertRaises(AccessDenied):
+            c.foo()
 
-        class TARA(object):
-            @decorators.auth_client(target=target)
-            def foo(self): pass
+    def test_auth_client(self):
+        class Backend(AuthBackend):
+            async def auth_client(self, controller, client_id, client_secret):
+                return client_id == "foo" and client_secret == "bar"
 
-            @decorators.auth_client(target=target_bad)
-            def foo_bad(self): pass
+        class MockObject(object):
+            @auth_client(backend_class=Backend)
+            async def foo(self): pass
 
         client_id = "foo"
         client_secret = "..."
-        r = endpoints.Request()
-        r.set_header('authorization', self.get_basic_auth_header(client_id, client_secret))
+        r = Request()
 
-        c = TARA()
+        c = self.mock_async(MockObject())
         c.request = r
-        with self.assertRaises(endpoints.AccessDenied):
+
+        r.set_header(
+            'authorization',
+            self.get_basic_auth_header(client_id, client_secret)
+        )
+        with self.assertRaises(AccessDenied):
             c.foo()
 
         client_secret = "bar"
-        r.set_header('authorization', self.get_basic_auth_header(client_id, client_secret))
+        r.set_header(
+            'authorization',
+            self.get_basic_auth_header(client_id, client_secret)
+        )
         c.foo()
 
-        with self.assertRaises(endpoints.AccessDenied):
-            c.foo_bad()
+    def test_auth_basic_simple(self):
+        class Backend(AuthBackend):
+            async def auth_basic(self, controller, username, password):
+                if username == "foo":
+                    raise ValueError()
 
-    def test_basic_auth_simple(self):
-        def target(controller, username, password):
-            if username != "bar":
-                raise ValueError()
-            return True
+                elif username == "bar":
+                    return True
 
-        def target_bad(controller, *args, **kwargs):
-            return False
+                else:
+                    return False
 
-        class TARA(object):
-            @decorators.auth_basic(target=target)
-            def foo(self): pass
-
-            @decorators.auth_basic(target=target_bad)
-            def foo_bad(self): pass
+        class MockObject(object):
+            @auth_basic(backend_class=Backend)
+            async def foo(self): pass
 
         username = "foo"
         password = "..."
-        r = endpoints.Request()
-        r.set_header('authorization', self.get_basic_auth_header(username, password))
-
-        c = TARA()
+        r = Request()
+        c = self.mock_async(MockObject())
         c.request = r
 
-        with self.assertRaises(endpoints.AccessDenied):
+        r.set_header(
+            'authorization',
+            self.get_basic_auth_header(username, password)
+        )
+        with self.assertRaises(AccessDenied):
             c.foo()
 
         username = "bar"
-        r.set_header('authorization', self.get_basic_auth_header(username, password))
+        r.set_header(
+            'authorization',
+            self.get_basic_auth_header(username, password)
+        )
         c.foo()
 
-        with self.assertRaises(endpoints.AccessDenied):
-            c.foo_bad()
+        username = "che"
+        r.set_header(
+            'authorization',
+            self.get_basic_auth_header(username, password)
+        )
+        with self.assertRaises(AccessDenied):
+            c.foo()
+
+    def test_auth_basic_same_kwargs(self):
+        class Backend(AuthBackend):
+            async def auth_basic(self, controller, username, password):
+                if username == "foo":
+                    raise ValueError()
 
-    def test_basic_auth_same_kwargs(self):
-        def target(controller, username, password):
-            if username != "bar":
-                raise ValueError()
-            return True
+                elif username == "bar":
+                    return True
+
+                else:
+                    return False
 
         class MockObject(object):
-            @decorators.auth_basic(target=target)
-            def foo(self, *args, **kwargs): return 1
+            @auth_basic(backend_class=Backend)
+            async def foo(self, **kwargs): return 1
 
-        c = MockObject()
+        c = self.mock_async(MockObject())
         username = "bar"
         password = "..."
-        r = endpoints.Request()
+        r = Request()
         r.set_header('authorization', self.get_basic_auth_header(username, password))
         c.request = r
 
@@ -431,40 +549,118 @@ def foo(self, *args, **kwargs): return 1
         r = c.foo(request="this_should_error_out")
         self.assertEqual(1, r)
 
-    def test_auth(self):
-        def target(controller, controller_args, controller_kwargs):
-            if controller.request.body_kwargs["foo"] != "bar":
-                raise ValueError()
-            return True
 
-        def target_bad(controller, *args, **kwargs):
-            return False
+    def test_auth_extend(self):
+        class Backend(AuthBackend):
+            async def auth(self, controller, controller_args, controller_kwargs):
+                if controller.request.body_kwargs["foo"] == "bar":
+                    return True
 
-        class TARA(object):
-            @decorators.auth(target=target)
-            def foo(self): pass
+                elif controller.request.body_kwargs["foo"] == "che":
+                    raise ValueError()
 
-            @decorators.auth(target=target_bad)
-            def foo_bad(self): pass
+                else:
+                    return False
 
-        r = endpoints.Request()
-        r.body_kwargs = {"foo": "che"}
+        class auth(AuthDecorator):
+            pass
 
-        c = TARA()
+        class MockObject(object):
+            @auth(backend_class=Backend)
+            def foo(self): pass
+
+        r = Request()
+        c = self.mock_async(MockObject())
         c.request = r
-        with self.assertRaises(endpoints.AccessDenied):
+
+        r.body_kwargs = {"foo": "che"}
+        with self.assertRaises(AccessDenied):
             c.foo()
 
         r.body_kwargs = {"foo": "bar"}
         c.foo()
 
 
+class CacheTest(TestCase):
+    def test_httpcache(self):
+        c = self.create_controller()
+
+        @httpcache(500)
+        async def afunc(self): pass
+        func = self.mock_async(afunc)
+
+        func(c)
+        h = c.response.get_header("Cache-Control")
+        self.assertTrue("max-age=500" in h)
+
+    def test_nohttpcache(self):
+        c = self.create_controller()
+
+        @nohttpcache
+        async def afunc(self): pass
+        func = self.mock_async(afunc)
+
+        func(c)
+        h = c.response.get_header("Cache-Control")
+        self.assertTrue("no-cache" in h)
+
+        h = c.response.get_header("Pragma")
+        self.assertTrue("no-cache" in h)
+
+
+class CodeErrorTest(TestCase):
+    def test_code_error(self):
+        c = self.create_controller()
+
+        @code_error(413, IOError)
+        async def func(self):
+            raise IOError()
+        func = self.mock_async(func)
+
+        with self.assertRaises(CallError) as e:
+            func(c)
+            self.assertEqual(413, e.code)
+
+    def test_raise(self):
+        c = self.create_server([
+            "from endpoints import Controller",
+            "from endpoints.decorators import code_error, param",
+            "",
+            "class Foo(Controller):",
+            "    @code_error(330, ValueError, IndexError)",
+            "    @param(",
+            "        0,",
+            "        metavar='error_type',",
+            "        choices=['value', 'index', 'another'],"
+            "    )",
+            "    def GET(self, error_type):",
+            "        if error_type.startswith('value'):",
+            "            raise ValueError()",
+            "        elif error_type.startswith('index'):",
+            "            raise IndexError()",
+            "        else:",
+            "            raise RuntimeError()",
+        ])
+
+        res = c.handle("/foo/value")
+        self.assertEqual(330, res.code)
+
+        res = c.handle("/foo/index")
+        self.assertEqual(330, res.code)
+
+        res = c.handle("/foo/another")
+        self.assertEqual(500, res.code)
+
+        res = c.handle("/foo/bar")
+        self.assertEqual(400, res.code)
+
+
 class ParamTest(TestCase):
     def test_type_string_casting(self):
         """I made a change in v4.0.0 that would encode a value to String when the 
         param type was a str descendent, but my change was bad because it would 
         just cast it to a String, not to the type, this makes sure that's fixed"""
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints.compat import String",
             "from endpoints import Controller, param",
             "",
@@ -486,7 +682,7 @@ def test_type_issue_76(self):
         """
         https://github.com/Jaymon/endpoints/issues/76
         """
-        c = Server("type_issue_76", [
+        c = self.create_server([
             "from endpoints import Controller, param",
             "",
             "class Query(object):",
@@ -516,7 +712,7 @@ def test_regex_issue_77(self):
         """
         https://github.com/Jaymon/endpoints/issues/77
         """
-        c = Server("regex_issue_77", [
+        c = self.create_server([
             "import datetime",
             "from endpoints import Controller, param",
             "",
@@ -534,15 +730,14 @@ def test_regex_issue_77(self):
         self.assertEqual(res._body.year, 2018)
         self.assertEqual(res._body.month, 1)
         self.assertEqual(res._body.day, 1)
-        #self.assertEqual("bar", res._body)
-
 
     def test_append_list_choices(self):
-        c = create_controller()
+        c = self.create_controller()
 
         @param('foo', action="append_list", type=int, choices=[1, 2])
         def foo(self, *args, **kwargs):
             return kwargs["foo"]
+        foo = self.mock_async(foo)
 
         #r = foo(c, **{'foo': "1,2"})
         r = foo(c, foo="1,2")
@@ -557,39 +752,26 @@ def foo(self, *args, **kwargs):
         with self.assertRaises(CallError):
             r = foo(c, **{'foo': 3})
 
-    def test_metavar(self):
-        raise SkipTest("not sure what to do with this test yet")
-        class MockMetavar(object):
-            request = Request()
-
-            @param(0, metavar="bar")
-            def foo(self, bar, **kwargs): return 1
-
-
-        o = MockMetavar()
-        o.request.method = 'GET'
-        o.request.path = "/0"
-
-        o.foo("0")
-
     def test_param_dest(self):
         """make sure the dest=... argument works"""
         # https://docs.python.org/2/library/argparse.html#dest
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', dest='bar')
+        @param('foo', dest='bar')
         def foo(self, *args, **kwargs):
             return kwargs.get('bar')
+        foo = self.mock_async(foo)
 
         r = foo(c, **{'foo': 1})
         self.assertEqual(1, r)
 
     def test_param_multiple_names(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', 'foos', 'foo3', type=int)
+        @param('foo', 'foos', 'foo3', type=int)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
 
         r = foo(c, **{'foo': 1})
         self.assertEqual(1, r)
@@ -600,31 +782,32 @@ def foo(self, *args, **kwargs):
         r = foo(c, **{'foo3': 3})
         self.assertEqual(3, r)
 
-        with self.assertRaises(endpoints.CallError):
+        with self.assertRaises(CallError):
             r = foo(c, **{'foo4': 0})
 
     def test_param_callable_default(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', default=time.time)
+        @param('foo', default=time.time)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
 
         start = time.time()
         r1 = foo(c, **{})
         self.assertLess(start, r1)
 
-        time.sleep(0.25)
+        time.sleep(0.1)
         r2 = foo(c, **{})
         self.assertLess(r1, r2)
 
-
     def test_param_not_required(self):
-        c = create_controller()
+        c = self.create_controller()
 
         @param('foo', required=False)
         def foo(self, *args, **kwargs):
             return 'foo' in kwargs
+        foo = self.mock_async(foo)
 
         r = foo(c, **{'foo': 1})
         self.assertTrue(r)
@@ -635,6 +818,7 @@ def foo(self, *args, **kwargs):
         @param('foo', required=False, default=5)
         def foo(self, *args, **kwargs):
             return 'foo' in kwargs
+        foo = self.mock_async(foo)
 
         r = foo(c, **{})
         self.assertTrue(r)
@@ -642,47 +826,50 @@ def foo(self, *args, **kwargs):
         @param('foo', type=int, required=False)
         def foo(self, *args, **kwargs):
             return 'foo' in kwargs
+        foo = self.mock_async(foo)
 
         r = foo(c, **{})
         self.assertFalse(r)
 
-
     def test_param_unicode(self):
-        c = create_controller()
-        r = endpoints.Request()
+        c = self.create_controller()
+        r = Request()
         r.set_header("content-type", "application/json;charset=UTF-8")
         charset = r.encoding
         c.request = r
-        #self.assertEqual("UTF-8", charset)
 
-        @endpoints.decorators.param('foo', type=str)
+        @param('foo', type=str)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
 
         words = testdata.get_unicode_words()
         ret = foo(c, **{"foo": words})
         self.assertEqual(String(ret), String(words))
 
-    def test_param(self):
-        c = create_controller()
+    def test_param_1(self):
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', type=int)
+        @param('foo', type=int)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
 
-        with self.assertRaises(endpoints.CallError):
+        with self.assertRaises(CallError):
             r = foo(c, **{'foo': 0})
 
-        @endpoints.decorators.param('foo', default=0)
+        @param('foo', default=0)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
 
         r = foo(c, **{})
         self.assertEqual(0, r)
 
-        @endpoints.decorators.param('foo', type=int, choices=set([1, 2, 3]))
+        @param('foo', type=int, choices=set([1, 2, 3]))
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
 
         c.request.method = 'POST'
         c.request.body_kwargs = {'foo': '1'}
@@ -694,12 +881,13 @@ def foo(self, *args, **kwargs):
         self.assertEqual(2, r)
 
     def test_post_param_body(self):
-        c = create_controller()
+        c = self.create_controller()
         c.request.method = 'POST'
 
         @param_body('foo', type=int, choices=set([1, 2, 3]))
         def foo(self, *args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
 
         with self.assertRaises(CallError):
             r = foo(c)
@@ -723,150 +911,169 @@ def foo(self, *args, **kwargs):
         self.assertEqual(3, r)
 
     def test_param_query(self):
-        c = create_controller()
+        c = self.create_controller()
 
         c.request.query_kwargs = {'foo': '8'}
-        @endpoints.decorators.param_query('foo', type=int, choices=set([1, 2, 3]))
+        @param_query('foo', type=int, choices=set([1, 2, 3]))
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         with self.assertRaises(endpoints.CallError):
             r = foo(c, **c.request.query_kwargs)
 
         c.request.query_kwargs = {'foo': '1'}
-        @endpoints.decorators.param_query('foo', type=int, choices=set([1, 2, 3]))
+        @param_query('foo', type=int, choices=set([1, 2, 3]))
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         r = foo(c, **c.request.query_kwargs)
         self.assertEqual(1, r)
 
         c.request.query_kwargs = {'foo': '1', 'bar': '1.5'}
-        @endpoints.decorators.param_query('foo', type=int)
-        @endpoints.decorators.param_query('bar', type=float)
+        @param_query('foo', type=int)
+        @param_query('bar', type=float)
         def foo(*args, **kwargs):
             return kwargs['foo'], kwargs['bar']
+        foo = self.mock_async(foo)
         r = foo(c, **c.request.query_kwargs)
         self.assertEqual(1, r[0])
         self.assertEqual(1.5, r[1])
 
         c.request.query_kwargs = {'foo': '1'}
-        @endpoints.decorators.param_query('foo', type=int, action='blah')
+        @param_query('foo', type=int, action='blah')
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         with self.assertRaises(RuntimeError):
             r = foo(c, **c.request.query_kwargs)
 
         c.request.query_kwargs = {'foo': ['1,2,3,4', '5']}
-        @endpoints.decorators.param_query('foo', type=int, action='store_list')
+        @param_query('foo', type=int, action='store_list')
         def foo(*args, **kwargs):
             return kwargs['foo']
-        with self.assertRaises(endpoints.CallError):
+        foo = self.mock_async(foo)
+        with self.assertRaises(CallError):
             r = foo(c, **c.request.query_kwargs)
 
         c.request.query_kwargs = {'foo': ['1,2,3,4', '5']}
-        @endpoints.decorators.param_query('foo', type=int, action='append_list')
+        @param_query('foo', type=int, action='append_list')
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         r = foo(c, **c.request.query_kwargs)
         self.assertEqual(list(range(1, 6)), r)
 
         c.request.query_kwargs = {'foo': '1,2,3,4'}
-        @endpoints.decorators.param_query('foo', type=int, action='store_list')
+        @param_query('foo', type=int, action='store_list')
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         r = foo(c, **c.request.query_kwargs)
         self.assertEqual(list(range(1, 5)), r)
 
         c.request.query_kwargs = {}
 
-        @endpoints.decorators.param_query('foo', type=int, default=1, required=False)
+        @param_query('foo', type=int, default=1, required=False)
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         r = foo(c)
         self.assertEqual(1, r)
 
-        @endpoints.decorators.param_query('foo', type=int, default=1, required=True)
+        @param_query('foo', type=int, default=1, required=True)
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         r = foo(c)
         self.assertEqual(1, r)
 
-        @endpoints.decorators.param_query('foo', type=int, default=1)
+        @param_query('foo', type=int, default=1)
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         r = foo(c)
         self.assertEqual(1, r)
 
-        @endpoints.decorators.param_query('foo', type=int)
+        @param_query('foo', type=int)
         def foo(*args, **kwargs):
             return kwargs['foo']
-        with self.assertRaises(endpoints.CallError):
+        foo = self.mock_async(foo)
+        with self.assertRaises(CallError):
             r = foo(c)
 
         c.request.query_kwargs = {'foo': '1'}
-        @endpoints.decorators.param_query('foo', type=int)
+        @param_query('foo', type=int)
         def foo(*args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
         r = foo(c, **c.request.query_kwargs)
         self.assertEqual(1, r)
 
     def test_param_size(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', type=int, min_size=100)
+        @param('foo', type=int, min_size=100)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
-        with self.assertRaises(endpoints.CallError):
+        foo = self.mock_async(foo)
+        with self.assertRaises(CallError):
             r = foo(c, **{'foo': 0})
         r = foo(c, **{'foo': 200})
         self.assertEqual(200, r)
 
-        @endpoints.decorators.param('foo', type=int, max_size=100)
+        @param('foo', type=int, max_size=100)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
-        with self.assertRaises(endpoints.CallError):
+        foo = self.mock_async(foo)
+        with self.assertRaises(CallError):
             r = foo(c, **{'foo': 200})
         r = foo(c, **{'foo': 20})
         self.assertEqual(20, r)
 
-        @endpoints.decorators.param('foo', type=int, min_size=100, max_size=200)
+        @param('foo', type=int, min_size=100, max_size=200)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
         r = foo(c, **{'foo': 120})
         self.assertEqual(120, r)
 
-        @endpoints.decorators.param('foo', type=str, min_size=2, max_size=4)
+        @param('foo', type=str, min_size=2, max_size=4)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
         r = foo(c, **{'foo': 'bar'})
         self.assertEqual('bar', r)
-        with self.assertRaises(endpoints.CallError):
+        with self.assertRaises(CallError):
             r = foo(c, **{'foo': 'barbar'})
 
     def test_param_lambda_type(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', type=lambda x: x.upper())
+        @param('foo', type=lambda x: x.upper())
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
         r = foo(c, **{'foo': 'bar'})
         self.assertEqual('BAR', r)
 
     def test_param_empty_default(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', default=None)
+        @param('foo', default=None)
         def foo(self, *args, **kwargs):
             return kwargs.get('foo')
+        foo = self.mock_async(foo)
         r = foo(c, **{})
         self.assertEqual(None, r)
 
     def test_param_reference_default(self):
-        c = create_controller()
+        c = self.create_controller()
 
         @param('foo', default={})
         def foo(self, *args, **kwargs):
             kwargs['foo'][testdata.get_ascii()] = testdata.get_ascii()
             return kwargs['foo']
+        foo = self.mock_async(foo)
 
         r = foo(c, **{})
         self.assertEqual(1, len(r))
@@ -874,10 +1081,11 @@ def foo(self, *args, **kwargs):
         r = foo(c, **{})
         self.assertEqual(1, len(r))
 
-        @endpoints.decorators.param('foo', default=[])
+        @param('foo', default=[])
         def foo(self, *args, **kwargs):
             kwargs['foo'].append(testdata.get_ascii())
             return kwargs['foo']
+        foo = self.mock_async(foo)
 
         r = foo(c, **{})
         self.assertEqual(1, len(r))
@@ -886,32 +1094,35 @@ def foo(self, *args, **kwargs):
         self.assertEqual(1, len(r))
 
     def test_param_regex(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', regex=r"^\S+@\S+$")
+        @param('foo', regex=r"^\S+@\S+$")
         def foo(self, *args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
 
         r = foo(c, **{'foo': 'foo@bar.com'})
 
-        with self.assertRaises(endpoints.CallError):
+        with self.assertRaises(CallError):
             r = foo(c, **{'foo': ' foo@bar.com'})
 
         @endpoints.decorators.param('foo', regex=re.compile(r"^\S+@\S+$", re.I))
         def foo(self, *args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
 
         r = foo(c, **{'foo': 'foo@bar.com'})
 
-        with self.assertRaises(endpoints.CallError):
+        with self.assertRaises(CallError):
             r = foo(c, **{'foo': ' foo@bar.com'})
 
     def test_param_bool(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', type=bool, allow_empty=True)
+        @param('foo', type=bool, allow_empty=True)
         def foo(self, *args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
 
         r = foo(c, **{'foo': 'true'})
         self.assertEqual(True, r)
@@ -931,30 +1142,33 @@ def foo(self, *args, **kwargs):
         r = foo(c, **{'foo': '0'})
         self.assertEqual(False, r)
 
-        @endpoints.decorators.param('bar', type=bool, require=True)
+        @param('bar', type=bool, require=True)
         def bar(self, *args, **kwargs):
             return kwargs['bar']
+        bar = self.mock_async(bar)
 
         r = bar(c, **{'bar': 'False'})
         self.assertEqual(False, r)
 
     def test_param_list(self):
-        c = create_controller()
+        c = self.create_controller()
 
-        @endpoints.decorators.param('foo', type=list)
+        @param('foo', type=list)
         def foo(self, *args, **kwargs):
             return kwargs['foo']
+        foo = self.mock_async(foo)
 
         r = foo(c, **{'foo': ['bar', 'baz']})
         self.assertEqual(r, ['bar', 'baz'])
 
     def test_param_arg(self):
         """Make sure positional args work"""
-        c = create_controller()
+        c = self.create_controller()
 
         @param(0)
         def foo(self, *args, **kwargs):
             return list(args)
+        foo = self.mock_async(foo)
 
         r = foo(c, 1)
         self.assertEqual([1], r)
@@ -966,6 +1180,7 @@ def foo(self, *args, **kwargs):
         @param(1, default=20, type=int)
         def foo(self, *args, **kwargs):
             return list(args)
+        foo = self.mock_async(foo)
 
         r = foo(c, 1)
         self.assertEqual(["1", 20], r)
@@ -979,6 +1194,7 @@ def foo(self, *args, **kwargs):
         def foo(self, *args, **kwargs):
             r = list(args) + [kwargs["foo"]]
             return r
+        foo = self.mock_async(foo)
 
         r = foo(c, 1, 2, foo="che")
         self.assertEqual(["1", 2, "che"], r)
@@ -991,9 +1207,9 @@ def foo(self, *args, **kwargs):
 
 
 class RouteTest(TestCase):
-    def test_issue94(self):
+    def test_issue_94(self):
         """https://github.com/Jaymon/endpoints/issues/94"""
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route, param",
             "class Default(Controller):",
@@ -1018,7 +1234,7 @@ def test_issue94(self):
         self.assertEqual("foo", res.body)
 
     def test_error(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route",
             "class Foo(Controller):",
@@ -1035,7 +1251,7 @@ def test_error(self):
         self.assertEqual(405, res.code)
 
     def test_path_route(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route_path",
             "class Foo(Controller):",
@@ -1061,7 +1277,7 @@ def test_path_route(self):
         self.assertEqual(404, res.code)
 
     def test_param_route_keys(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route_param",
             "class Foo(Controller):",
@@ -1087,7 +1303,7 @@ def test_param_route_keys(self):
         self.assertEqual(400, res.code)
 
     def test_param_route_matches(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route_param",
             "class Foo(Controller):",
@@ -1113,7 +1329,7 @@ def test_param_route_matches(self):
         self.assertEqual(400, res.code)
 
     def test_simple(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route",
             "class Foo(Controller):",
@@ -1146,7 +1362,7 @@ def test_simple(self):
         self.assertEqual(4, res._body)
 
     def test_extend_1(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route",
             "",
@@ -1203,7 +1419,7 @@ def test_extend_1(self):
             self.assertEqual(5, res._body)
 
     def test_extend_2(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route",
             "",
@@ -1224,7 +1440,7 @@ def test_extend_2(self):
 
     def test_mixed(self):
         """make sure plays nice with param"""
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import route, param",
             "class Foo(Controller):",
@@ -1253,8 +1469,7 @@ def test_mixed(self):
 
 class VersionTest(TestCase):
     def test_simple(self):
-        controller_prefix = "version_simple"
-        c = Server(controller_prefix, [
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import version",
             "class Foo(Controller):",
@@ -1283,7 +1498,7 @@ def test_simple(self):
         self.assertEqual(404, res.code)
 
     def test_complex(self):
-        c = Server(contents=[
+        c = self.create_server([
             "from endpoints import Controller",
             "from endpoints.decorators import version, route",
             "",
@@ -1319,34 +1534,3 @@ def test_complex(self):
         res = c.handle("/foo/2", version="v2")
         self.assertEqual(22, res._body)
 
-
-class CodeErrorTest(TestCase):
-    def test_raise(self):
-        c = Server(contents=[
-            "from endpoints import Controller",
-            "from endpoints.decorators import code_error, param",
-            "",
-            "class Foo(Controller):",
-            "    @code_error(330, ValueError, IndexError)",
-            "    @param(0, metavar='error_type', choices=['value', 'index', 'another'])",
-            "    def GET(self, error_type):",
-            "        if error_type.startswith('value'):",
-            "            raise ValueError()",
-            "        elif error_type.startswith('index'):",
-            "            raise IndexError()",
-            "        else:",
-            "            raise RuntimeError()",
-        ])
-
-        res = c.handle("/foo/value")
-        self.assertEqual(330, res.code)
-
-        res = c.handle("/foo/index")
-        self.assertEqual(330, res.code)
-
-        res = c.handle("/foo/another")
-        self.assertEqual(500, res.code)
-
-        res = c.handle("/foo/bar")
-        self.assertEqual(400, res.code)
-
diff --git a/tests/extras/__init__.py b/tests/extras/__init__.py
deleted file mode 100644
index c60116d..0000000
--- a/tests/extras/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-
diff --git a/tests/extras/html_test.py b/tests/extras/html_test.py
deleted file mode 100644
index 46e7172..0000000
--- a/tests/extras/html_test.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from endpoints.compat import *
-from .. import TestCase, testdata, Server
-
-
-class DecoratorsTest(TestCase):
-    def test_html(self):
-
-        p = testdata.create_file("foo.html", contents=[
-            '<h1>Hello {{ name }}</h1>',
-        ])
-
-        c = Server(contents=[
-            "from endpoints import Controller",
-            "from endpoints.extras.html.decorators import view",
-            "class Default(Controller):",
-            "    @view('{}', directories=['{}'])".format(p.fileroot, p.parent),
-            "    def GET(self):",
-            "        return {'name': 'foo bar'}"
-        ])
-        res = c.handle('/')
-        self.assertTrue("Hello foo bar" in res._body)
-
diff --git a/tests/http_test.py b/tests/http_test.py
deleted file mode 100644
index 408d531..0000000
--- a/tests/http_test.py
+++ /dev/null
@@ -1,336 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import json
-import io
-
-from requests.auth import _basic_auth_str
-
-from endpoints.compat import *
-from endpoints.http import Headers, Url, Response, Request, Environ, Body
-from endpoints.utils import String, ByteString
-from . import testdata, TestCase, Server
-
-
-class BodyTest(TestCase):
-    def test_bad_content_type(self):
-        """make sure a form upload content type with json body fails correctly"""
-        r = Request()
-        r.environ["REQUEST_METHOD"] = "POST"
-
-        b = b"plain text body"
-        r.headers.update({'content-length': String(len(b))})
-        r.headers.pop("content-type", None)
-        body = Body(io.BytesIO(b), r)
-        self.assertEqual(b, body.file.read())
-
-        b = b"foo=bar&che=baz&foo=che"
-        r.headers.update({'content-type': 'application/json', 'content-length': String(len(b))})
-        if is_py2:
-            with self.assertRaises(ValueError):
-                body = Body(io.BytesIO(b), r)
-
-        else:
-            with self.assertRaises(json.JSONDecodeError):
-                body = Body(io.BytesIO(b), r)
-
-        b = b'{"foo": ["bar", "che"], "che": "baz"}'
-        r.headers.update({'content-type': "application/x-www-form-urlencoded", 'content-length': String(len(b))})
-        with self.assertRaises(ValueError):
-            body = Body(io.BytesIO(b), r)
-
-
-class RequestTest(TestCase):
-    def test_get_auth_scheme(self):
-        r = Request()
-        r.set_headers({
-            "Authorization": "Basic FOOBAR",
-        })
-
-        self.assertEqual("Basic", r.get_auth_scheme())
-        self.assertTrue(r.is_auth("basic"))
-        self.assertTrue(r.is_auth("Basic"))
-        self.assertFalse(r.is_auth("bearer"))
-
-        r.headers["Authorization"] = "BLAH_TOKEN"
-        self.assertEqual("", r.get_auth_scheme())
-        self.assertFalse(r.is_auth("basic"))
-
-    def test_is_oauth(self):
-        username = "foo"
-        password = "bar"
-        r = Request()
-
-        r.set_headers({"Authorization": _basic_auth_str(username, password)})
-        self.assertTrue(r.is_oauth("basic"))
-        self.assertTrue(r.is_oauth("client"))
-        self.assertFalse(r.is_oauth("token"))
-        self.assertFalse(r.is_oauth("access"))
-
-        r.headers["Authorization"] = "Bearer FOOBAR"
-        self.assertFalse(r.is_oauth("basic"))
-        self.assertFalse(r.is_oauth("client"))
-        self.assertTrue(r.is_oauth("token"))
-        self.assertTrue(r.is_oauth("access"))
-
-        r.headers.pop("Authorization")
-        self.assertFalse(r.is_oauth("basic"))
-        self.assertFalse(r.is_oauth("client"))
-        self.assertFalse(r.is_oauth("token"))
-        self.assertFalse(r.is_oauth("access"))
-
-    def test_override(self):
-
-        class RO(Request):
-            def __setattr__(self, k, v):
-                super(RO, self).__setattr__(k, v)
-                #pout.v(k, v)
-                #self.__dict__[k] = v
-
-        req = RO()
-        req.path = "/foo/bar"
-        #pout.v(req.path, req)
-
-    def test_copy(self):
-        r = Request()
-        r.set_headers({
-            "Host": "localhost",
-        })
-        r.query = "foo=bar"
-        r.path = "/baz/che"
-        r.environ['wsgi.url_scheme'] = "http"
-        r.environ['SERVER_PORT'] = "80"
-        r.foo = 1
-
-        r2 = r.copy()
-        self.assertEqual(r.foo, r2.foo)
-        self.assertEqual(r.environ["SERVER_PORT"], r2.environ["SERVER_PORT"])
-
-        r2.headers["foo"] = "bar"
-        self.assertFalse("foo" in r.headers)
-        self.assertTrue("foo" in r2.headers)
-
-    def test_url(self):
-        """make sure the .url attribute is correctly populated"""
-        # this is wsgi configuration
-        r = Request()
-        r.set_headers({
-            "Host": "localhost",
-        })
-        r.query = "foo=bar"
-        r.path = "/baz/che"
-        r.environ['wsgi.url_scheme'] = "http"
-        r.environ['SERVER_PORT'] = "80"
-        u = r.url
-        self.assertEqual("http://localhost/baz/che?foo=bar", r.url)
-        r.port = 555
-        u = r.url
-        self.assertEqual("http://localhost:555/baz/che?foo=bar", r.url)
-
-        # handle proxied connections
-        r.host = "localhost:10000"
-        r.port = "9000"
-        u = r.url
-        self.assertTrue(":10000" in u)
-
-    def test_charset(self):
-        r = Request()
-        r.set_header("content-type", "application/json;charset=UTF-8")
-        charset = r.encoding
-        self.assertEqual("UTF-8", charset)
-
-        r = Request()
-        r.set_header("content-type", "application/json")
-        charset = r.encoding
-        self.assertEqual(None, charset)
-
-    def test_ip(self):
-        r = Request()
-        r.set_header('REMOTE_ADDR', '172.252.0.1')
-        self.assertEqual('172.252.0.1', r.ip)
-
-        r = Request()
-        r.set_header('REMOTE_ADDR', '1.241.34.107')
-        self.assertEqual('1.241.34.107', r.ip)
-
-        r = Request()
-        r.set_header('x-forwarded-for', '54.241.34.107')
-        self.assertEqual('54.241.34.107', r.ip)
-
-        r.set_header('x-forwarded-for', '127.0.0.1, 54.241.34.107')
-        self.assertEqual('54.241.34.107', r.ip)
-
-        r.set_header('x-forwarded-for', '127.0.0.1')
-        r.set_header('client-ip', '54.241.34.107')
-        self.assertEqual('54.241.34.107', r.ip)
-
-    def test_ip_bad(self):
-        r = Request()
-        r.set_header('REMOTE_ADDR', "10.0.2.2")
-        r.set_header("Via", "1.1 ironport1.orlando.cit:80 (Cisco-WSA/9.0.1-162)")
-        self.assertEqual("", r.ip)
-
-        r = Request()
-        r.set_header('REMOTE_ADDR', "54.241.34.107")
-        r.set_header("Via", "1.1 ironport1.orlando.cit:80 (Cisco-WSA/9.0.1-162)")
-        self.assertEqual("54.241.34.107", r.ip)
-
-    def test_properties(self):
-
-        path = '/foo/bar'
-        path_args = ['foo', 'bar']
-
-        r = Request()
-        r.path = path
-        self.assertEqual(r.path, path)
-        self.assertEqual(r.path_args, path_args)
-
-        r = Request()
-        r.path_args = path_args
-        self.assertEqual(r.path, path)
-        self.assertEqual(r.path_args, path_args)
-
-        query = "foo=bar&che=baz&foo=che"
-        query_kwargs = {'foo': ['bar', 'che'], 'che': 'baz'}
-
-        r = Request()
-        r.query = query
-        self.assertEqual(parse.parse_qs(r.query, True), parse.parse_qs(query, True))
-        self.assertEqual(r.query_kwargs, query_kwargs)
-
-        r = Request()
-        r.query_kwargs = query_kwargs
-        self.assertEqual(parse.parse_qs(r.query, True), parse.parse_qs(query, True))
-        self.assertEqual(r.query_kwargs, query_kwargs)
-
-    def test_get_header(self):
-        r = Request()
-
-        r.set_headers({
-            'foo': 'bar',
-            'Content-Type': 'application/json',
-            'Happy-days': 'are-here-again'
-        })
-        v = r.get_header('foo', 'che')
-        self.assertEqual('bar', v)
-
-        v = r.get_header('Foo', 'che')
-        self.assertEqual('bar', v)
-
-        v = r.get_header('FOO', 'che')
-        self.assertEqual('bar', v)
-
-        v = r.get_header('che', 'che')
-        self.assertEqual('che', v)
-
-        v = r.get_header('che')
-        self.assertEqual(None, v)
-
-        v = r.get_header('content-type')
-        self.assertEqual('application/json', v)
-
-        v = r.get_header('happy-days')
-        self.assertEqual('are-here-again', v)
-
-
-class ResponseTest(TestCase):
-    def test_headers(self):
-        """make sure headers don't persist between class instantiations"""
-        r = Response()
-        r.headers["foo"] = "bar"
-        self.assertEqual("bar", r.headers["foo"])
-        self.assertEqual(1, len(r.headers))
-
-        r = Response()
-        self.assertFalse("foo" in r.headers)
-        self.assertEqual(0, len(r.headers))
-
-    def test_status(self):
-        r = Response()
-        for code, status in BaseHTTPRequestHandler.responses.items():
-            r.code = code
-            self.assertEqual(status[0], r.status)
-            r.code = None
-            r.status = None
-
-        r = Response()
-        r.code = 1000
-        self.assertEqual("UNKNOWN", r.status)
-
-    def test_body_file(self):
-        r = Response()
-        self.assertFalse("Content-Type" in r.headers)
-        self.assertFalse("Content-Length" in r.headers)
-
-        path = testdata.create_file("foo.jpg", "12345")
-        with path.open() as fp:
-            r.body = fp
-            mt = r.headers["Content-Type"]
-            fs = r.headers["Content-Length"]
-            self.assertEqual("image/jpeg", mt)
-            self.assertEqual(5, int(fs))
-
-        path = testdata.create_file("foo.txt", "123")
-        with path.open() as fp:
-            r.body = fp
-            mt = r.headers["Content-Type"]
-            fs = r.headers["Content-Length"]
-            self.assertEqual("text/plain", mt)
-            self.assertEqual(3, int(fs))
-
-    def test_code(self):
-        r = Response()
-        self.assertEqual(204, r.code)
-
-        r.body = "this is the body"
-        self.assertEqual(200, r.code)
-
-        r.code = 404
-        self.assertEqual(404, r.code)
-
-        r.body = "this is the body 2"
-        self.assertEqual(404, r.code)
-
-        r.body = None
-        self.assertEqual(404, r.code)
-
-        # now let's test defaults
-        del(r._code)
-
-        self.assertEqual(204, r.code)
-
-        r.body = ''
-        self.assertEqual(200, r.code)
-
-        r.body = {}
-        self.assertEqual(200, r.code)
-
-
-class UrlTest(TestCase):
-    def test_module(self):
-        c = Server(contents={
-            "foo": [
-                "from endpoints import Controller",
-                "class Bar(Controller):",
-                "    def GET(self):",
-                "        u = self.request.url",
-                "        return u.module()",
-                "",
-                "class Default(Controller):",
-                "    def GET(self):",
-                "        u = self.request.url",
-                "        return u.module()",
-                "",
-            ],
-        })
-
-        res = c.handle("/foo/bar")
-        self.assertEqual("http://endpoints.fake/foo", res._body)
-
-        res = c.handle("/foo")
-        self.assertEqual("http://endpoints.fake/foo", res._body)
-
-    def test_controller(self):
-        u = Url("http://example.com/foo/bar/che", class_path="foo")
-        u2 = u.controller(che=4)
-        self.assertEqual("http://example.com/foo?che=4", u2)
-
diff --git a/tests/interface/__init__.py b/tests/interface/__init__.py
index 16fac42..51241d4 100644
--- a/tests/interface/__init__.py
+++ b/tests/interface/__init__.py
@@ -8,14 +8,13 @@
 from unittest import TestSuite
 
 from endpoints.compat import *
-from endpoints.client import WebClient, WebsocketClient
+from endpoints.client import HTTPClient, WebSocketClient
+from endpoints.interface.base import BaseApplication
 from .. import TestCase as BaseTestCase
 
 
 class TestCase(BaseTestCase):
-    server = None
-    server_class = None # this should be a client.Server class
-    client_class = WebClient
+    client_class = HTTPClient
 
     def setUp(self):
         if self.server:
@@ -25,64 +24,121 @@ def tearDown(self):
         if self.server:
             self.server.stop()
 
-    def create_server(self, contents, config_contents='', **kwargs):
-        tdm = testdata.create_module(kwargs.get("controller_prefix", ""), contents)
-
-        kwargs["controller_prefix"] = tdm
-        kwargs["host"] = self.get_host()
-        kwargs["cwd"] = tdm.basedir
-
-        if config_contents:
-            config_path = testdata.create_file("{}.py".format(testdata.get_module_name()), config_contents)
-            kwargs["config_path"] = config_path
-
-        server = self.server_class(**kwargs)
-        server.stop()
-        server.start()
-        self.server = server
-        return server
-
     def create_client(self, **kwargs):
         kwargs.setdefault("host", self.server.host)
         client = self.client_class(**kwargs)
         return client
 
 
-class WebTestCase(TestCase):
-    def test_body_plain_with_content_type(self):
+class _HTTPTestCase(TestCase):
+    def test_get_request_url(self):
+        """make sure request url gets controller_path correctly"""
+        server = self.create_server(contents=[
+            "from endpoints import Controller",
+            "class Requrl(Controller):",
+            "    def GET(self):",
+            "        return self.request.url.controller()",
+            "",
+        ])
+
+        c = self.create_client()
+        r = c.get('/requrl')
+        self.assertTrue("/requrl" in r.body)
+        self.assertRegex(r.body, r"https?://[^/]")
+
+    def test_get_list_param_decorator(self):
+        server = self.create_server(contents=[
+            "from endpoints import Controller, decorators",
+            "class Listparamdec(Controller):",
+            "    @decorators.param(",
+            "        'user_ids',",
+            "        'user_ids[]',",
+            "        type=int,",
+            "        action='append_list'",
+            "    )",
+            "    def GET(self, **kwargs):",
+            "        return int(''.join(map(str, kwargs['user_ids'])))",
+            ""
+        ])
+
+        c = self.create_client()
+        r = c.get('/listparamdec?user_ids[]=12&user_ids[]=34')
+        self.assertEqual("1234", r.body)
+
+    def test_get_404_request(self):
+        server = self.create_server(contents=[
+            "from endpoints import Controller",
+            "class Foo(Controller):",
+            "    def GET(self, **kwargs): pass",
+            "",
+        ])
+
+        c = self.create_client()
+        r = c.get('/foo/bar/baz?che=1&boo=2')
+        self.assertEqual(404, r.code)
+
+    def test_get_response_headers(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
             "class Default(Controller):",
-            "    def POST(self, **kwargs):",
-            "        self.response.headers['content-type'] = 'text/plain'",
-            "        return self.request.body.read()",
+            "    def GET(self):",
+            "        self.response.set_header('FOO_BAR', 'check')",
+            "",
         ])
 
-        body = "plain text body"
-        c = self.create_client(headers={"content-type": "text/plain"})
-        r = c.post("/", body)
+        c = self.create_client()
+        r = c.get('/')
+        self.assertEqual(204, r.code)
+        self.assertTrue("foo-bar" in r.headers)
+
+    def test_get_file_stream(self):
+        content = "this is a text file to stream"
+        filepath = testdata.create_file(content)
+        server = self.create_server(contents=[
+            "from endpoints import Controller",
+            "class Default(Controller):",
+            "    def GET(self, *args, **kwargs):",
+            "        f = open('{}')".format(filepath),
+            "        self.response.set_header('content-type', 'text/plain')",
+            "        return f",
+            "",
+        ])
+
+        c = self.create_client()
+        r = c.get('/')
         self.assertEqual(200, r.code)
-        self.assertEqual(body, r.body)
-        self.assertEqual(String(body), String(r._body))
+        self.assertEqual(content, r.body)
+
+    def test_get_generators(self):
+        server = self.create_server(contents=[
+            "from endpoints import Controller",
+            "class Default(Controller):",
+            "    def GET(self):",
+            "        for x in range(100):",
+            "            yield x",
+        ])
+
+        c = self.create_client()
+        r = c.get('/')
+        content = list(range(100))
+        self.assertEqual(200, r.code)
+        self.assertEqual(content, r._body)
 
-    def test_body_plain_without_content_type(self):
+    def test_post_body_urlencoded(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
             "class Default(Controller):",
             "    def POST(self, **kwargs):",
-            "        self.response.headers['content-type'] = 'text/plain'",
-            "        return self.request.body.read()",
+            "        return kwargs",
         ])
 
-        body = "plain text body"
-        #c = self.create_client(headers={"content-type": "text/plain"})
+        body = {"foo": "1", "bar": ["2", "3"], "che": "four"}
         c = self.create_client()
         r = c.post("/", body)
         self.assertEqual(200, r.code)
-        self.assertEqual(body, r.body)
-        self.assertEqual(String(body), String(r._body))
+        self.assertEqual(body, r._body)
 
-    def test_body_json_dict(self):
+    def test_post_body_json_dict(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
             "class Default(Controller):",
@@ -96,7 +152,7 @@ def test_body_json_dict(self):
         self.assertEqual(body, r._body["kwargs"])
         self.assertEqual(0, len(r._body["args"]))
 
-    def test_body_json_list(self):
+    def test_post_body_json_list(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
             "class Default(Controller):",
@@ -115,24 +171,38 @@ def test_body_json_list(self):
         self.assertEqual(1, r._body["args"][0]["foo"])
         self.assertEqual(2, len(r._body["args"]))
 
-    def test_body_file_1(self):
-        filepath = testdata.create_file("filename.txt", "this is a text file to upload")
+    def test_post_body_file_1(self):
+        filepath = testdata.create_file(
+            "this is a text file to upload",
+            ext="txt"
+        )
         server = self.create_server(contents=[
             "from endpoints import Controller",
             "class Default(Controller):",
             "    def POST(self, *args, **kwargs):",
-            "        return kwargs['file'].filename",
+            "        return {",
+            "            'filename': kwargs['file'].filename,",
+            "            'foo': kwargs['foo'],",
+            "            'baz': kwargs['baz'],",
+            "        }",
             "",
         ])
 
+        body = {"foo": "value-foo", "baz": "value-baz"}
         c = self.create_client()
-        r = c.post_file('/', {"foo": "bar", "baz": "che"}, {"file": filepath})
+        r = c.post_file(
+            '/',
+            body,
+            {"file": filepath}
+        )
         self.assertEqual(200, r.code)
-        self.assertTrue("filename.txt" in r.body)
+        self.assertTrue(filepath.name in r.body)
+        for k, v in body.items():
+            self.assertTrue(v in r.body)
 
-    def test_body_file_2(self):
+    def test_post_body_file_2(self):
         """make sure specifying a @param for the file upload works as expected"""
-        filepath = testdata.create_file("post_file_with_param.txt", "post_file_with_param")
+        filepath = testdata.create_file("post_file_with_param")
         server = self.create_server(contents=[
             "from endpoints import Controller, decorators",
             "class Default(Controller):",
@@ -144,39 +214,74 @@ def test_body_file_2(self):
         ])
 
         c = self.create_client()
-        r = c.post_file('/', {"foo": "bar", "baz": "che"}, {"file": filepath})
+        r = c.post_file(
+            '/',
+            {"foo": "value-foo", "baz": "value-baz"},
+            {"file": filepath}
+        )
         self.assertEqual(200, r.code)
-        self.assertTrue("post_file_with_param.txt" in r.body)
+        self.assertTrue(filepath.name in r.body)
 
-    def test_request_url(self):
-        """make sure request url gets controller_path correctly"""
+    def test_post_body_plain_with_content_type(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
-            "class Requrl(Controller):",
-            "    def GET(self):",
-            "        return self.request.url.controller()",
-            "",
+            "class Default(Controller):",
+            "    def POST(self, **kwargs):",
+            "        self.response.headers['content-type'] = 'text/plain'",
+            "        return self.request.body",
         ])
 
-        c = self.create_client()
-        r = c.get('/requrl')
-        self.assertTrue("/requrl" in r._body)
+        body = "plain text body"
+        c = self.create_client(headers={"content-type": "text/plain"})
+        r = c.post("/", body)
+        self.assertEqual(200, r.code)
+        self.assertEqual(body, r.body)
+        self.assertEqual(String(body), String(r._body))
 
-    def test_list_param_decorator(self):
+    def test_post_body_plain_without_content_type(self):
         server = self.create_server(contents=[
-            "from endpoints import Controller, decorators",
-            "class Listparamdec(Controller):",
-            "    @decorators.param('user_ids', 'user_ids[]', type=int, action='append_list')",
-            "    def GET(self, **kwargs):",
-            "        return int(''.join(map(str, kwargs['user_ids'])))",
-            ""
+            "from endpoints import Controller",
+            "class Default(Controller):",
+            "    def POST(self, **kwargs):",
+            "        self.response.headers['content-type'] = 'text/plain'",
+            "        return self.request.body",
         ])
 
+        body = "plain text body"
+        #c = self.create_client(headers={"content-type": "text/plain"})
         c = self.create_client()
-        r = c.get('/listparamdec?user_ids[]=12&user_ids[]=34')
-        self.assertEqual("1234", r.body)
+        r = c.post("/", body)
+        self.assertEqual(200, r.code)
+        self.assertEqual(body, r.body)
+        self.assertEqual(String(body), String(r._body))
 
-    def test_post_basic(self):
+    def test_response_body_1(self):
+        server = self.create_server(contents=[
+            "from endpoints import Controller",
+            "class Default(Controller):",
+            "    def POST(self, **kwargs):",
+            "        content_type = '{};charset={}'.format(",
+            "            kwargs['content_type'],",
+            "            self.encoding",
+            "        )",
+            "        self.response.set_header('content-type', content_type)",
+            "        return kwargs['body']",
+        ])
+
+        body = {'foo': testdata.get_words()}
+        c = self.create_client(json=True)
+
+        r = c.post('/', {'content_type': 'plain/text', 'body': body})
+        self.assertEqual(ByteString(body), r._body)
+        self.assertEqual(String(body), r.body)
+
+        r = c.post('/', {'content_type': 'application/json', 'body': body})
+        self.assertEqual(json.dumps(body), r.body)
+
+        r = c.post('/', {'content_type': 'application/json', 'body': {}})
+        self.assertEqual("{}", r.body)
+
+    def test_versioning(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
             "from endpoints.decorators import version",
@@ -189,6 +294,13 @@ def test_post_basic(self):
             "",
         ])
 
+        c = self.create_client()
+        r = c.post('/', None, headers={"content-type": "application/json"})
+        self.assertEqual(204, r.code)
+        return
+
+
+
         c = self.create_client()
         r = c.post(
             '/',
@@ -213,173 +325,193 @@ def test_post_basic(self):
         r = c.post('/', {}, headers={"content-type": "application/json"})
         self.assertEqual(204, r.code)
 
-        r = c.post('/', {"foo": "bar"}, headers={"Accept": "application/json;version=v2"})
+        r = c.post(
+            '/',
+            {"foo": "bar"},
+            headers={"Accept": "application/json;version=v2"}
+        )
         self.assertEqual(200, r.code)
         self.assertEqual('"bar"', r.body)
 
-    def test_404_request(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "class Foo(Controller):",
-            "    def GET(self, **kwargs): pass",
-            "",
-        ])
 
-        c = self.create_client()
-        r = c.get('/foo/bar/baz?che=1&boo=2')
-        self.assertEqual(404, r.code)
+class _WebSocketTestCase(TestCase):
+    client_class = WebSocketClient
+    server_class = None
 
-    def test_response_headers(self):
-        server = self.create_server(contents=[
+    def test_connect_success(self):
+        server = self.create_server([
             "from endpoints import Controller",
             "class Default(Controller):",
-            "    def GET(self):",
-            "        self.response.set_header('FOO_BAR', 'check')",
-            "",
+            "    def CONNECT(self, *args, **kwargs):",
+            "        pass",
+            "    def DISCONNECT(self, *args, **kwargs):",
+            "        pass",
         ])
 
         c = self.create_client()
-        r = c.get('/')
+        r = c.connect(trace=True)
         self.assertEqual(204, r.code)
-        self.assertTrue("foo-bar" in r.headers)
+        self.assertTrue(c.connected)
+        c.close()
+        self.assertFalse(c.connected)
 
-    def test_file_stream(self):
-        content = "this is a text file to stream"
-        filepath = testdata.create_file("filename.txt", content)
+    def test_connect_error(self):
         server = self.create_server(contents=[
-            "from endpoints import Controller",
+            "from endpoints import Controller, CallError",
+            "from endpoints.decorators.auth import client_auth as BaseAuth",
+            "class client_auth(BaseAuth):",
+            "    def target(self, *args, **kwargs): return False",
+            "",
             "class Default(Controller):",
-            "    def GET(self, *args, **kwargs):",
-            "        f = open('{}')".format(filepath),
-            "        self.response.set_header('content-type', 'text/plain')",
-            "        return f",
+            "    def CONNECT(self, *args, **kwargs):",
+            "        auth = client_auth()",
+            "        auth.handle_target(self.request, args, kwargs)",
+            "",
+            "    def DISCONNECT(self, *args, **kwargs): pass",
             "",
         ])
 
         c = self.create_client()
-        r = c.get('/')
-        self.assertEqual(200, r.code)
-        self.assertEqual(content, r.body)
-        #self.assertTrue(r.body)
+        with self.assertRaises(IOError):
+            c.connect()
 
-    def test_generators(self):
+    def test_connect_failure(self):
         server = self.create_server(contents=[
-            "from endpoints import Controller",
+            "from endpoints import Controller, CallError",
             "class Default(Controller):",
-            "    def GET(self):",
-            "        for x in range(100):",
-            "            yield x",
+            "    def CONNECT(self, *args, **kwargs):",
+            "        raise CallError(401, 'this is the message')",
+            "    def DISCONNECT(self, *args, **kwargs):",
+            "        pass",
         ])
 
         c = self.create_client()
-        r = c.get('/')
-        content = list(range(100))
-        self.assertEqual(200, r.code)
-        self.assertEqual(content, r._body)
+        with self.assertRaises(IOError):
+            c.connect()
 
-    def test_response_body_1(self):
+    def test_close_connection(self):
         server = self.create_server(contents=[
-            "from endpoints import Controller",
+            "from endpoints import Controller, CloseConnection",
             "class Default(Controller):",
-            "    def POST(self, **kwargs):",
-            "        content_type = '{};charset={}'.format(kwargs['content_type'], self.encoding)",
-            "        self.response.set_header('content-type', content_type)",
-            "        return kwargs['body']",
+            "    def CONNECT(self, **kwargs):",
+            "        pass",
+            "    def DISCONNECT(self, **kwargs):",
+            "        pass",
+            "    def GET(self, **kwargs):",
+            "        raise CloseConnection()",
         ])
 
-        body = {'foo': testdata.get_words()}
-        c = self.create_client(json=True)
-
-        r = c.post('/', {'content_type': 'plain/text', 'body': body})
-        self.assertEqual(ByteString(body), r._body)
-        self.assertEqual(String(body), r.body)
-
-        r = c.post('/', {'content_type': 'application/json', 'body': body})
-        self.assertEqual(json.dumps(body), r.body)
-
-        r = c.post('/', {'content_type': 'application/json', 'body': {}})
-        self.assertEqual("{}", r.body)
-
-    def test_response_body_json_error(self):
-        """I was originally going to have the body method smother the error, but
-        after thinking about it a little more, I think it is better to bubble up
-        the error and rely on the user to handle it in their code"""
-
-        # 1-13-2021 update, turns out response body is buried, an error is
-        # raised but the server returns a 200 because the headers are already
-        # sent before the body is encoded, so all the headers are sent but body
-        # is empty
-        self.skip_test("")
+        c = self.create_client()
+        c.connect()
+        with self.assertRaises(RuntimeError):
+            c.get("/", timeout=10, attempts=1)
 
+    def test_no_support(self):
         server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "class Default(Controller):",
-            "    def GET(self):",
-            "        class Foo(object): pass",
-            "        return {'foo': Foo()}",
+            "from endpoints import Controller, CallError",
+            "class Confetch(Controller):",
+            "    def GET(self, **kwargs):",
+            "        pass",
         ])
 
         c = self.create_client()
-        r = c.get('/')
-        pout.v(r.code, r.body)
-        return
-
-
+        with self.assertRaises(IOError):
+            c.connect()
 
-        self.skip_test("moved from http.ResponseTest, make this work at some point")
-        class Foo(object): pass
-        b = {'foo': Foo()}
+    def test_path_autoconnect(self):
+        server = self.create_server(contents=[
+            "from endpoints import Controller",
+            "",
+            "class Foo(Controller):",
+            "    def CONNECT(self): pass",
+            "    def DISCONNECT(self): pass",
+            "",
+            "    def POST(self, **kwargs):",
+            "        return 1",
+            "",
+        ])
 
-        r = Response()
-        r.headers['Content-Type'] = 'application/json'
-        r.body = b
-        with self.assertRaises(TypeError):
-            rb = r.body
+        c = self.create_client()
+        r = c.post("/foo", {"bar": 2})
+        self.assertEqual(1, r._body)
 
+    def test_get_query(self):
+        server = self.create_server(contents=[
+            "from endpoints import Controller",
+            "",
+            "class Default(Controller):",
+            "    def CONNECT(self, *args, **kwargs): pass",
+            "    def DISCONNECT(self, *args, **kwargs): pass",
+            "",
+            "    def GET(self, **kwargs):",
+            "        return kwargs['foo']",
+        ])
 
-class WebsocketTestCase(TestCase):
-    client_class = WebsocketClient
-    server_class = None
+        c = self.create_client()
+        r = c.get("/", {"foo": 2})
+        self.assertEqual(200, r.code)
+        self.assertEqual(2, r._body)
 
-    def test_bad_path(self):
-        """https://github.com/Jaymon/endpoints/issues/103"""
+    def test_request_basic(self):
         server = self.create_server(contents=[
-            "import os",
-            "from endpoints import Controller, decorators",
+            "from endpoints import Controller",
+            "",
             "class Default(Controller):",
-            "    def CONNECT(self, **kwargs):",
-            "        pass",
-            "    def DISCONNECT(self, **kwargs):",
-            "        pass",
-            "    def GET(self, foo, bar):",
-            "        return 'get'",
+            "    def CONNECT(self, *args, **kwargs): pass",
+            "    def DISCONNECT(self, *args, **kwargs): pass",
             "",
+            "    def SOCKET(self, *args, **kwargs):",
+            "        return {",
+            "            'name': 'SOCKET',",
+            "            'args': args,",
+            "            'kwargs': kwargs,",
+            "        }",
+            "    def POST(self, *args, **kwargs):",
+            "        return {",
+            "            'name': 'POST',",
+            "            'args': args,",
+            "            'kwargs': kwargs,",
+            "        }",
+            "    def GET(self, *args, **kwargs):",
+            "        return {",
+            "            'name': 'GET',",
+            "            'args': args,",
+            "            'kwargs': kwargs,",
+            "        }",
         ])
+
         c = self.create_client()
-        c.connect()
+        r = c.post("/foo/bar", {"val1": 1, "val2": 2})
+        self.assertEqual("POST", r._body["name"])
 
-        r = c.get("http://example.com/foo/bar")
-        self.assertEqual(404, r.code)
+        r = c.send("/foo/bar", {"val1": 1, "val2": 2})
+        self.assertEqual("SOCKET", r._body["name"])
+        self.assertEqual({"val1": 1, "val2": 2}, r._body["kwargs"])
 
-        r = c.get("/foo/bar")
-        self.assertEqual(200, r.code)
+        r = c.get("/foo/bar", {"val1": 1, "val2": 2})
+        self.assertEqual("GET", r._body["name"])
 
-    def test_close_connection(self):
+    def test_multiple_connections(self):
         server = self.create_server(contents=[
-            "from endpoints import Controller, CloseConnection",
+            "from endpoints import Controller",
             "class Default(Controller):",
-            "    def CONNECT(self, **kwargs):",
-            "        pass",
-            "    def DISCONNECT(self, **kwargs):",
-            "        pass",
+            "    def CONNECT(self, **kwargs): pass",
+            "    def DISCONNECT(self, **kwargs): pass",
             "    def GET(self, **kwargs):",
-            "        raise CloseConnection()",
+            "        return self.request.uuid",
         ])
 
-        c = self.create_client()
-        c.connect()
-        with self.assertRaises(RuntimeError):
-            c.get("/", timeout=0.1, attempts=1)
+        count = 10
+        cs = []
+        for x in range(count):
+            c = self.create_client()
+            c.connect()
+            cs.append(c)
+
+        for x in range(count):
+            for c in cs:
+                r = c.get("/")
+                self.assertEqual(c.client_id, r.body)
 
     def test_rapid_requests(self):
         """We were dropping requests when making a whole bunch of websocket
@@ -390,10 +522,8 @@ def test_rapid_requests(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
             "class Default(Controller):",
-            "    def CONNECT(self, **kwargs):",
-            "        pass",
-            "    def DISCONNECT(self, **kwargs):",
-            "        pass",
+            "    def CONNECT(self, **kwargs): pass",
+            "    def DISCONNECT(self, **kwargs): pass",
             "    def GET(self, **kwargs):",
             "        return kwargs['pid']",
         ])
@@ -421,6 +551,31 @@ def target(x):
 
             self.assertEqual(set([0, 1, 2, 3, 4]), set(rs))
 
+    def test_bad_path(self):
+        """https://github.com/Jaymon/endpoints/issues/103"""
+        server = self.create_server(contents=[
+            "import os",
+            "from endpoints import Controller, decorators",
+            "class Default(Controller):",
+            "    def CONNECT(self, **kwargs):",
+            "        pass",
+            "    def DISCONNECT(self, **kwargs):",
+            "        pass",
+            "    def GET(self, foo, bar):",
+            "        return 'get'",
+            "",
+        ])
+        c = self.create_client()
+        c.connect()
+
+        pout.b()
+
+        r = c.get("http://example.com/foo/bar")
+        self.assertEqual(404, r.code)
+
+        r = c.get("/foo/bar")
+        self.assertEqual(200, r.code)
+
     def test_path_mixup(self):
         """Jarid was hitting this problem, we were only able to get it to happen
         consistently with his environment, the problem stemmed from one request
@@ -453,10 +608,8 @@ def test_versioning(self):
             "from endpoints import Controller",
             "from endpoints.decorators import version",
             "class Default(Controller):",
-            "    def CONNECT(self, **kwargs):",
-            "        pass",
-            "    def DISCONNECT(self, **kwargs):",
-            "        pass",
+            "    def CONNECT(self, **kwargs): pass",
+            "    def DISCONNECT(self, **kwargs): pass",
             "    @version('', 'v1')",
             "    def GET_v1(*args, **kwargs): return 'v1'",
             "    @version('v2')",
@@ -466,6 +619,10 @@ def test_versioning(self):
         c = self.create_client()
         c.connect()
 
+        r = c.get('/')
+        self.assertEqual(200, r.code)
+        self.assertTrue("v1" in r.body)
+
         r = c.get(
             '/',
             headers={
@@ -484,177 +641,6 @@ def test_versioning(self):
         self.assertEqual(200, r.code)
         self.assertTrue("v2" in r.body)
 
-        r = c.get('/')
-        self.assertEqual(200, r.code)
-        self.assertTrue("v1" in r.body)
-
-
-    def test_connect_on_fetch(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller, CallError",
-            "class Confetch(Controller):",
-            "    def CONNECT(self, **kwargs):",
-            "        if int(kwargs['foo']) != 1:",
-            "            raise CallError(400)",
-            "    def GET(self, **kwargs):",
-            "        pass",
-        ])
-
-        c = self.create_client()
-        r = c.get("/confetch", {"foo": 1, "bar": 2})
-        self.assertEqual(204, r.code)
-
-        c = self.create_client()
-        with self.assertRaises(RuntimeError):
-            r = c.get("/confetch", {"foo": 2})
-
-    def test_get_fetch_host(self):
-        client_cls = self.client_class
-        c = client_cls("http://localhost")
-        self.assertTrue(c.get_fetch_host().startswith("ws"))
-
-        c = client_cls("https://localhost")
-        self.assertTrue(c.get_fetch_host().startswith("wss"))
-
-        c = client_cls("HTTPS://localhost")
-        self.assertTrue(c.get_fetch_host().startswith("wss"))
-
-        c = client_cls("HTTP://localhost")
-        self.assertTrue(c.get_fetch_host().startswith("ws"))
-
-    def test_connect_success(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "class Default(Controller):",
-            "    def CONNECT(self, *args, **kwargs):",
-            "        pass",
-            "    def DISCONNECT(self, *args, **kwargs):",
-            "        pass",
-        ])
-
-        c = self.create_client()
-        r = c.connect(trace=True)
-        self.assertEqual(204, r.code)
-        self.assertTrue(c.connected)
-
-        # when looking at logs, this test looks like there is a problem because
-        # right after connection an IOError is thrown, that's because the close
-        # will cause uWSGI to raise an IOError, giving the websocket a chance
-        # to clean up the connection
-
-        c.close()
-        self.assertFalse(c.connected)
-
-    def test_connect_failure(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller, CallError",
-            "class Default(Controller):",
-            "    def CONNECT(self, *args, **kwargs):",
-            "        raise CallError(401, 'this is the message')",
-            "    def DISCONNECT(self, *args, **kwargs):",
-            "        pass",
-        ])
-
-        c = self.create_client()
-        with self.assertRaises(IOError):
-            c.connect()
-
-    def test_request_basic(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "",
-            "class Default(Controller):",
-            "    def CONNECT(self, *args, **kwargs):",
-            "        #pout.v(args, kwargs, self.request)",
-            "        #pout.b('CONNECT')",
-            "        pass",
-            "    def DISCONNECT(self, *args, **kwargs):",
-            "        #pout.b('DISCONNECT')",
-            "        pass",
-            "",
-            "    def SOCKET(self, *args, **kwargs):",
-            "        #pout.v(args, kwargs)",
-            "        #pout.b('SOCKET')",
-            "        return {",
-            "            'name': 'SOCKET',",
-            "            'args': args,",
-            "            'kwargs': kwargs,",
-            "        }",
-            "    def POST(self, *args, **kwargs):",
-            "        return {",
-            "            'name': 'POST',",
-            "            'args': args,",
-            "            'kwargs': kwargs,",
-            "        }",
-            "    def GET(self, *args, **kwargs):",
-            "        return {",
-            "            'name': 'GET',",
-            "            'args': args,",
-            "            'kwargs': kwargs,",
-            "        }",
-        ])
-
-        c = self.create_client()
-        r = c.post("/foo/bar", {"val1": 1, "val2": 2})
-        self.assertEqual("POST", r._body["name"])
-
-        r = c.send("/foo/bar", {"val1": 1, "val2": 2})
-        self.assertEqual("SOCKET", r._body["name"])
-        self.assertEqual({"val1": 1, "val2": 2}, r._body["kwargs"])
-
-        r = c.get("/foo/bar", {"val1": 1, "val2": 2})
-        self.assertEqual("GET", r._body["name"])
-
-    def test_request_modification(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "",
-            "class Default(Controller):",
-            "    def CONNECT(self):",
-            "        self.request.foo = 1",
-            "",
-            "    def DISCONNECT(self, *args, **kwargs):",
-            "        pass",
-            "",
-            "    def POST(self, **kwargs):",
-            "        self.request.parent.foo = kwargs['foo']",
-            "        return self.request.parent.foo",
-            "",
-            "    def GET(self):",
-            "        return self.request.foo",
-        ])
-
-        c = self.create_client()
-        r = c.post("/", {"foo": 2})
-        self.assertEqual(2, r._body)
-
-        r = c.get("/")
-        self.assertEqual(2, r._body)
-
-        r = c.post("/", {"foo": 4})
-        self.assertEqual(4, r._body)
-
-        r = c.get("/")
-        self.assertEqual(4, r._body)
-
-    def test_path_autoconnect(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "",
-            "class Foo(Controller):",
-            "    def CONNECT(self): pass",
-            "    def DISCONNECT(self): pass",
-            "",
-            "    def POST(self, **kwargs):",
-            "        return 1",
-            "",
-        ])
-
-        c = self.create_client()
-        c.basic_auth("foo", "bar")
-        r = c.post("/foo", {"bar": 2})
-        self.assertEqual(1, r._body)
-
     def test_error_500(self):
         server = self.create_server(contents=[
             "from endpoints import Controller",
@@ -692,104 +678,3 @@ def test_call_error(self):
         r = c.get("/")
         self.assertEqual(401, r.code)
 
-    def test_connect_error(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller, CallError",
-            "from endpoints.decorators.auth import client_auth as BaseAuth",
-            "class client_auth(BaseAuth):",
-            "    def target(self, *args, **kwargs): return False",
-            "",
-            "class Default(Controller):",
-            "    def CONNECT(self, *args, **kwargs):",
-            "        auth = client_auth()",
-            "        auth.handle_target(self.request, args, kwargs)",
-            "",
-            "    def DISCONNECT(self, *args, **kwargs): pass",
-            "",
-        ])
-
-        c = self.create_client()
-        with self.assertRaises(IOError):
-            c.connect()
-            #r = c.get("/")
-
-    def test_get_query(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "",
-            "class Default(Controller):",
-            "    def CONNECT(self, *args, **kwargs): pass",
-            "    def DISCONNECT(self, *args, **kwargs): pass",
-            "",
-            "    def GET(self, **kwargs):",
-            "        return kwargs['foo']",
-        ])
-
-        c = self.create_client()
-        r = c.get("/", {"foo": 2})
-        self.assertEqual(200, r.code)
-        self.assertEqual(2, r._body)
-
-    def test_count(self):
-        self.skip_test("count is no longer being sent down")
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "",
-            "class Default(Controller):",
-            "    def CONNECT(self, *args, **kwargs): pass",
-            "    def DISCONNECT(self, *args, **kwargs): pass",
-            "",
-            "    def GET(self, **kwargs): pass",
-            "    def POST(self, **kwargs): pass",
-        ])
-
-        c = self.create_client()
-        for x in range(2, 7):
-            r = getattr(c, random.choice(["get", "post"]))("/")
-            self.assertEqual(204, r.code)
-            self.assertEqual(x, r.count)
-
-        c.close()
-
-        for x in range(2, 7):
-            r = getattr(c, random.choice(["get", "post"]))("/")
-            self.assertEqual(204, r.code)
-            self.assertEqual(x, r.count)
-
-
-class WebServerTestCase(TestCase):
-    """Tests the client.Webserver for the interface"""
-    def test_start(self):
-        server = self.create_server(contents=[
-            "from endpoints import Controller",
-            "class Default(Controller):",
-            "    def GET(self): pass",
-            "",
-        ])
-
-    def test_file(self):
-        server = self.create_server(
-            contents=[
-                "import os",
-                "from endpoints import Controller, decorators",
-                "class Default(Controller):",
-                "    def GET(self):",
-                "        return os.environ['WSGI_TESTING']",
-                "",
-            ],
-            config_contents=[
-                "import os",
-                "os.environ['WSGI_TESTING'] = 'foo bar'",
-                "",
-            ]
-        )
-        c = self.create_client()
-
-        r = c.get("/")
-        self.assertEqual(200, r.code)
-        self.assertEqual("foo bar", r._body)
-
-
-def load_tests(*args, **kwargs):
-    return TestSuite()
-
diff --git a/tests/interface/asgi_test.py b/tests/interface/asgi_test.py
new file mode 100644
index 0000000..695a3e2
--- /dev/null
+++ b/tests/interface/asgi_test.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+import re
+
+from datatypes import (
+    Command,
+    Host,
+)
+
+from endpoints.interface.asgi import Application
+from endpoints.config import environ
+
+from . import _HTTPTestCase, _WebSocketTestCase
+
+
+class Server(Command):
+    """
+    https://github.com/django/daphne
+    """
+    @property
+    def environ(self):
+        env = super().environ
+        for env_name in environ.get_prefix_names("ENDPOINTS_PREFIX"):
+            env.pop(env_name)
+        env["ENDPOINTS_PREFIX"] = self.controller_prefix
+        return env
+
+    def __init__(self, controller_prefix, host="", **kwargs):
+        self.controller_prefix = controller_prefix
+        if not host:
+            host = environ.HOST
+        self.host = Host(host) if host else None
+
+        cmd_host = "0.0.0.0"
+        cmd_port = "4000"
+        if self.host:
+            cmd_host = self.host.hostname
+            cmd_port = self.host.port
+
+        app_path = "endpoints.interface.asgi:ApplicationFactory"
+        super().__init__(
+            f"daphne -b {cmd_host} -p {cmd_port} -v 3 {app_path}",
+            **kwargs
+        )
+
+    def start(self, **kwargs):
+        super().start(**kwargs)
+
+        regex = re.compile(r"Listening\s+on\s+TCP\s+address\s+(([^:]+):(\d+))")
+        r = self.wait_for(regex)
+        m = regex.search(r)
+        self.host = Host(m.group(2), m.group(3)).client()
+
+
+class HTTPApplicationTest(_HTTPTestCase):
+    server_class = Server
+    application_class = Application
+
+
+class WebSocketApplicationTest(_WebSocketTestCase):
+    server_class = Server
+    application_class = Application
+
diff --git a/tests/interface/base_test.py b/tests/interface/base_test.py
new file mode 100644
index 0000000..2a73934
--- /dev/null
+++ b/tests/interface/base_test.py
@@ -0,0 +1,575 @@
+# -*- coding: utf-8 -*-
+
+from endpoints.call import Controller
+
+from . import TestCase
+
+
+class BaseApplicationTest(TestCase):
+    def test_routing_error_unexpected_args(self):
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(foo=2))
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(che=2))
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(409, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo", query_kwargs=dict(bar=2))
+        self.assertEqual(405, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar")
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo, **kwargs):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(404, res.code)
+
+    def test_routing_error_unexpected_args(self):
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(foo=2))
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(che=2))
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(409, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo", query_kwargs=dict(bar=2))
+        self.assertEqual(405, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar")
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, foo, **kwargs):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(404, res.code)
+
+    def test_routing_error_no_args(self):
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self):",
+            "        pass",
+        ])
+        res = c.handle("/foo/bar/che/baz/boom/bam/blah")
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, **kwargs):",
+            "        pass",
+        ])
+        res = c.handle(
+            "/foo/bar/che/baz/boom/bam/blah",
+            query_kwargs=dict(foo=1, bar=2)
+        )
+        self.assertEqual(404, res.code)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self, **kwargs):",
+            "        return kwargs",
+        ])
+        res = c.handle("/", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(200, res.code)
+        self.assertTrue("foo" in res._body)
+
+        c = self.create_server(contents=[
+            "from endpoints import Controller, param",
+            "class Default(Controller):",
+            "    def GET(self):",
+            "        return kwargs",
+        ])
+        res = c.handle("/", query_kwargs=dict(foo=1, bar=2))
+        self.assertEqual(405, res.code)
+
+    def test_lowercase_method(self):
+        c = self.create_server(contents={"foo2": [
+            "from endpoints import Controller",
+            "class Bar(Controller):",
+            "    def get(*args, **kwargs): pass"
+        ]})
+
+        res = c.handle("/foo2/bar", query_kwargs={'foo2': 'bar', 'che': 'baz'})
+        self.assertEqual(501, res.code)
+
+    def test_handle_redirect(self):
+        c = self.create_server(contents={"handle": [
+            "from endpoints import Controller, Redirect",
+            "class Testredirect(Controller):",
+            "    def GET(*args, **kwargs):",
+            "        raise Redirect('http://example.com')"
+        ]})
+
+        res = c.handle("/handle/testredirect")
+        self.assertEqual(302, res.code)
+        self.assertEqual('http://example.com', res.headers['Location'])
+
+    def test_handle_404_typeerror_1(self):
+        """make sure not having a controller is correctly identified as a 404"""
+        c = self.create_server(contents=[
+            "from endpoints import Controller, Redirect",
+            "class NoFoo(Controller):",
+            "    def GET(*args, **kwargs):",
+            "        pass",
+        ])
+
+        res = c.handle("/foo/boom")
+        self.assertEqual(404, res.code)
+
+    def test_handle_405_typeerror_2(self):
+        """make sure 405 works when a path bit is missing"""
+        c = self.create_server(contents=[
+            "from endpoints import Controller, decorators",
+            "class Default(Controller):",
+            "    def GET(self, needed_bit, **kwargs):",
+            "       return ''",
+            "",
+            "    def POST(self, needed_bit, **kwargs):",
+            "       return ''",
+            "",
+            "class Htype(Controller):",
+            "    def POST(self, needed_bit, **kwargs):",
+            "       return ''",
+            "",
+            "class Hdec(Controller):",
+            "    @decorators.param('foo', default='bar')",
+            "    def POST(self, needed_bit, **kwargs):",
+            "       return ''",
+            "",
+        ])
+
+        res = c.handle("/hdec", "POST")
+        self.assertEqual(405, res.code)
+
+        res = c.handle("/htype", "POST")
+        self.assertEqual(405, res.code)
+
+        res = c.handle("/")
+        self.assertEqual(405, res.code)
+
+        res = c.handle("/", "POST")
+        self.assertEqual(405, res.code)
+
+    def test_handle_404_typeerror_3(self):
+        """there was an error when there was only one expected argument, turns out
+        the call was checking for "arguments" when the message just had "argument"
+        """
+        c = self.create_server(contents=[
+            "from endpoints import Controller",
+            "class Foo(Controller):",
+            "    def GET(self): pass",
+            "",
+        ])
+
+        res = c.handle("/foo/bar/baz", query='che=1&boo=2')
+        self.assertEqual(404, res.code)
+
+    def test_handle_accessdenied(self):
+        """raising an AccessDenied error should set code to 401 and the correct
+        header"""
+        c = self.create_server(contents=[
+            "from endpoints import Controller, AccessDenied",
+            "class Default(Controller):",
+            "    def GET(*args, **kwargs):",
+            "        raise AccessDenied(scheme='basic')",
+            "class Bar(Controller):",
+            "    def GET(*args, **kwargs):",
+            "        raise AccessDenied()",
+        ])
+
+        res = c.handle("/")
+        self.assertEqual(401, res.code)
+        self.assertTrue('Basic' in res.headers['WWW-Authenticate'])
+
+        res = c.handle("/bar")
+        self.assertEqual(401, res.code)
+        self.assertTrue('Auth' in res.headers['WWW-Authenticate'])
+
+    def test_handle_callstop(self):
+        c = self.create_server(contents=[
+            "from endpoints import Controller, CallStop",
+            "class Testcallstop(Controller):",
+            "    def GET(*args, **kwargs):",
+            "        raise CallStop(205, None)",
+            "",
+            "class Testcallstop2(Controller):",
+            "    def GET(*args, **kwargs):",
+            "        raise CallStop(200, 'this is the body')",
+            "",
+            "class Testcallstop3(Controller):",
+            "    def GET(*args, **kwargs):",
+            "        raise CallStop(204, 'this is ignored')",
+        ])
+
+        res = c.handle("/testcallstop")
+        self.assertEqual(None, res.body)
+        self.assertEqual(None, res._body)
+        self.assertEqual(205, res.code)
+
+        res = c.handle("/testcallstop2")
+        self.assertEqual("this is the body", res.body)
+        self.assertEqual(200, res.code)
+
+        res = c.handle("/testcallstop3")
+        self.assertEqual(None, res._body)
+        self.assertEqual(204, res.code)
+
+    def test_path_errors(self):
+        c = self.create_server([
+            "from endpoints import Controller",
+            "class Foo(Controller):",
+            "    def GET(self, bar): pass",
+            "",
+        ])
+
+        # this path is technically valid but is missing required arguments
+        res = c.handle("/foo", "GET")
+        self.assertEqual(405, res.code)
+
+        # there is no POST method for this path
+        res = c.handle("/foo/bar", "POST")
+        self.assertEqual(404, res.code)
+
+        # there is no GET method that has this path
+        res = c.handle("/foo/bar/che", "GET")
+        self.assertEqual(404, res.code)
+
+    def test_handle_method_chain(self):
+        c = self.create_server(contents=[
+            "from endpoints import Controller",
+            "class Foo(Controller):",
+            "    async def handle(self, *args, **kwargs):",
+            "        self.response.handle_called = True",
+            "        await super().handle(*args, **kwargs)",
+            "",
+            "    def GET(self, *args, **kwargs):",
+            "        self.response.GET_called = True",
+            "",
+        ])
+
+        res = c.handle("/foo")
+        self.assertTrue(res.handle_called)
+        self.assertTrue(res.GET_called)
+
+    def test_multiple_controller_prefixes_1(self):
+        self.create_modules({
+            "foo": [
+                "from endpoints import Controller",
+                "class Default(Controller): pass",
+            ],
+            "bar": [
+                "from endpoints import Controller",
+                "class User(Controller): pass",
+            ],
+        })
+
+        c = self.create_server(controller_prefixes=["foo", "bar"])
+
+        t = c.find("/user")
+        self.assertTrue("bar", t["module_name"])
+
+        t = c.find("/che")
+        self.assertTrue("foo", t["module_name"])
+
+    def test_routing_1(self):
+        """there was a bug that caused errors raised after the yield to return
+        another iteration of a body instead of raising them"""
+        contents = [
+            "from endpoints import Controller",
+            "class Default(Controller):",
+            "    def GET(self): pass",
+            "",
+            "class Foo(Controller):",
+            "    def GET(self): pass",
+            "",
+            "class Bar(Controller):",
+            "    def GET(self): pass",
+        ]
+        controller_prefix = self.create_module(contents=contents)
+        c = self.create_server(controller_prefixes=[controller_prefix])
+
+        info = c.find()
+        self.assertEqual(info['module_name'], controller_prefix)
+        self.assertEqual(info['class_name'], "Default")
+
+        info = c.find("/foo/che/baz")
+        self.assertEqual(2, len(info['method_args']))
+        self.assertEqual(info['class_name'], "Foo")
+
+    def test_default_match_with_path(self):
+        """when the default controller is used, make sure it falls back to
+        default class name if the path bit fails to be a controller class name"""
+        c = self.create_server({
+            "nmcon": [
+                "from endpoints import Controller",
+                "class Default(Controller):",
+                "    def GET(self, *args, **kwargs):",
+                "        return args[0]"
+            ]
+        })
+
+        res = c.handle("/nmcon/8")
+        self.assertEqual("8", res.body)
+
+    def test_no_match(self):
+        """make sure a controller module that imports a class with the same as
+        one of the query args doesen't get picked up as the controller class"""
+        c = self.create_server({
+            "nomod": [
+                "class Nomodbar(object): pass",
+            ],
+            "": [
+                "from endpoints import Controller",
+                "from .nomod import Nomodbar",
+                "class Default(Controller):",
+                "    def GET(): pass",
+            ],
+        })
+
+        path = '/nomodbar' # same name as one of the non controller classes
+        info = c.find(path)
+        self.assertEqual('Default', info['class_name'])
+        self.assertEqual(c.controller_prefix, info['module_name'])
+        self.assertEqual('nomodbar', info['method_args'][0])
+
+    def test_import_error(self):
+        c = self.create_server([
+            "from endpoints import Controller",
+            "from does_not_exist import FairyDust",
+            "class Default(Controller):",
+            "    def GET(): pass",
+        ])
+        res = c.handle('/')
+        self.assertEqual(404, res.code)
+
+    def test_callback_info(self):
+        c = self.create_server()
+        request = c.create_request("/foo/bar", "GET")
+        request.query_kwargs = {'foo': 'bar', 'che': 'baz'}
+        with self.assertRaises(TypeError):
+            c.find(request=request)
+
+        c = self.create_server({
+            "foo": [
+                "from endpoints import Controller",
+                "class Bar(Controller):",
+                "    def GET(*args, **kwargs): pass"
+            ],
+        })
+
+        # if it succeeds, then it passed the test :)
+        d = c.find(request=request)
+
+    def test_get_controller_info_default(self):
+        """I introduced a bug on 1-12-14 that caused default controllers to fail
+        to be found, this makes sure that bug is squashed"""
+        c = self.create_server([
+            "from endpoints import Controller",
+            "class Default(Controller):",
+            "    def GET(): pass",
+        ])
+
+        info = c.find("/")
+        self.assertEqual('Default', info['class_name'])
+        self.assertTrue(issubclass(info['class'], Controller))
+
+    def test_get_controller_info_advanced(self):
+        c = self.create_server({
+            "": [
+                "from endpoints import Controller",
+                "class Default(Controller):",
+                "    def GET(*args, **kwargs): pass",
+                ""
+            ],
+            "default": [
+                "from endpoints import Controller",
+                "class Default(Controller):",
+                "    def GET(*args, **kwargs): pass",
+                ""
+            ],
+            "foo": [
+                "from endpoints import Controller",
+                "class Default(Controller):",
+                "    def GET(*args, **kwargs): pass",
+                "",
+                "class Bar(Controller):",
+                "    def GET(*args, **kwargs): pass",
+                "    def POST(*args, **kwargs): pass",
+                ""
+            ],
+            "foo.baz": [
+                "from endpoints import Controller",
+                "class Default(Controller):",
+                "    def GET(*args, **kwargs): pass",
+                "",
+                "class Che(Controller):",
+                "    def GET(*args, **kwargs): pass",
+                ""
+            ],
+            "foo.boom": [
+                "from endpoints import Controller",
+                "",
+                "class Bang(Controller):",
+                "    def GET(*args, **kwargs): pass",
+                ""
+            ],
+        })
+
+        ts = [
+            {
+                'in': dict(method="GET", path="/foo/bar/happy/sad"),
+                'out': {
+                    'module_name': f"{c.controller_prefix}.foo",
+                    'class_name': 'Bar',
+                    'method_args': ['happy', 'sad'],
+                }
+            },
+            {
+                'in': dict(method="GET", path="/"),
+                'out': {
+                    'module_name': f"{c.controller_prefix}",
+                    'class_name': 'Default',
+                    'method_args': [],
+                }
+            },
+            {
+                'in': dict(method="GET", path="/happy"),
+                'out': {
+                    'module_name': f"{c.controller_prefix}",
+                    'class_name': 'Default',
+                    'method_args': ["happy"],
+                }
+            },
+            {
+                'in': dict(method="GET", path="/foo/baz"),
+                'out': {
+                    'module_name': f"{c.controller_prefix}.foo.baz",
+                    'class_name': 'Default',
+                    'method_args': [],
+                }
+            },
+            {
+                'in': dict(method="GET", path="/foo/baz/che"),
+                'out': {
+                    'module_name': f"{c.controller_prefix}.foo.baz",
+                    'class_name': 'Che',
+                    'method_args': [],
+                }
+            },
+            {
+                'in': dict(method="GET", path="/foo/baz/happy"),
+                'out': {
+                    'module_name': f"{c.controller_prefix}.foo.baz",
+                    'class_name': 'Default',
+                    'method_args': ["happy"],
+                }
+            },
+            {
+                'in': dict(method="GET", path="/foo/happy"),
+                'out': {
+                    'module_name': f"{c.controller_prefix}.foo",
+                    'class_name': 'Default',
+                    'method_args': ["happy"],
+                }
+            },
+        ]
+
+        for t in ts:
+            request = c.create_request(**t['in'])
+            d = c.find(request=request)
+            for key, val in t['out'].items():
+                self.assertEqual(val, d[key])
+
diff --git a/tests/interface/tornado_test.py b/tests/interface/tornado_test.py
deleted file mode 100644
index c7faf41..0000000
--- a/tests/interface/tornado_test.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from endpoints.interface.tornado.client import WebServer, WebsocketServer
-from . import WebTestCase, WebsocketTestCase, WebServerTestCase
-
-
-class WebTest(WebTestCase):
-    server_class = WebServer
-
-
-class WebsocketTest(WebsocketTestCase):
-    server_class = WebsocketServer
-
-
-class WebServerTest(WebServerTestCase):
-    server_class = WebServer
-
-
-del WebTestCase
-del WebsocketTestCase
-del WebServerTestCase
-
diff --git a/tests/interface/uwsgi_test.py b/tests/interface/uwsgi_test.py
deleted file mode 100644
index 5745432..0000000
--- a/tests/interface/uwsgi_test.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-import testdata
-
-from endpoints.compat import *
-from endpoints.interface.uwsgi.client import WebServer, WebsocketServer
-from . import WebTestCase, WebServerTestCase, WebsocketTestCase
-
-
-class WebTest(WebTestCase):
-
-    server_class = WebServer
-
-    def create_server(self, contents, config_contents='', **kwargs):
-        config_contents = [
-            "import os",
-            "import sys",
-            "import logging",
-            "logging.basicConfig(",
-            "    format=\"[%(levelname).1s] %(message)s\",",
-            "    level=logging.DEBUG,",
-            "    stream=sys.stdout",
-            ")",
-            "",
-            "from endpoints.interface.uwsgi import Application",
-            ""
-            "##############################################################",
-            config_contents if isinstance(config_contents, basestring) else "\n".join(config_contents),
-            "##############################################################",
-            "application = Application()",
-            ""
-        ]
-
-        return super(WebTest, self).create_server(
-            contents=contents,
-            config_contents=config_contents,
-        )
-
-
-class WebsocketTest(WebsocketTestCase):
-    server_class = WebsocketServer
-
-    def create_server(self, contents, config_contents='', **kwargs):
-        config_contents = [
-            "import os",
-            "import sys",
-            "import logging",
-            "logging.basicConfig(",
-            "    format=\"[%(levelname).1s] %(message)s\",",
-            "    level=logging.DEBUG,",
-            "    stream=sys.stdout",
-            ")",
-            "",
-            #"import gevent",
-            #"import gevent.monkey",
-            #"if not gevent.monkey.saved:",
-            #"    gevent.monkey.patch_all()",
-            "",
-            "from endpoints.interface.uwsgi.gevent import WebsocketApplication as Application",
-            "",
-            "##############################################################",
-            config_contents if isinstance(config_contents, basestring) else "\n".join(config_contents),
-            "##############################################################",
-            "application = Application()",
-            ""
-        ]
-
-        return super(WebsocketTest, self).create_server(
-            contents=contents,
-            config_contents=config_contents,
-        )
-
-
-class WebServerTest(WebServerTestCase, WebTest):
-    pass
-
-
-del WebTestCase
-del WebsocketTestCase
-del WebServerTestCase
-
diff --git a/tests/interface/wsgi_test.py b/tests/interface/wsgi_test.py
index 83b78a7..f461945 100644
--- a/tests/interface/wsgi_test.py
+++ b/tests/interface/wsgi_test.py
@@ -1,18 +1,53 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals, division, print_function, absolute_import
+import re
 
-from endpoints.interface.wsgi.client import WebServer
-from . import WebTestCase, WebServerTestCase
+from datatypes import ModuleCommand, Host
 
+from . import _HTTPTestCase
+from endpoints.config import environ
 
-class WebTest(WebTestCase):
-    server_class = WebServer
 
+class Server(ModuleCommand):
+    @property
+    def environ(self):
+        env = super().environ
+        for env_name in environ.get_prefix_names("ENDPOINTS_PREFIX"):
+            env.pop(env_name)
+        env["ENDPOINTS_PREFIX"] = self.controller_prefix
+        return env
 
-class WebServerTest(WebServerTestCase):
-    server_class = WebServer
+    def __init__(self, controller_prefix, host="", **kwargs):
+        self.controller_prefix = controller_prefix
+        if not host:
+            host = environ.HOST
+        self.host = Host(host) if host else None
 
+        cmd_host = "0.0.0.0:4000"
+        if self.host:
+            cmd_host = self.host.netloc
 
-del WebTestCase
-del WebServerTestCase
+        cmd = [
+            "--host", cmd_host,
+            "--prefix", self.controller_prefix,
+            "--server", "endpoints.interface.wsgi:Server",
+        ]
+
+        super().__init__(
+            "endpoints",
+            command=cmd,
+            **kwargs
+        )
+
+    def start(self, **kwargs):
+        super().start(**kwargs)
+
+        regex = re.compile(r"Listening\s+on\s+(([^:]+):(\d+))")
+        r = self.wait_for(regex)
+        m = regex.search(r)
+        self.host = Host(m.group(2), m.group(3)).client()
+
+
+class HTTPTest(_HTTPTestCase):
+    server_class = Server
 
diff --git a/tests/reflection_test.py b/tests/reflection_test.py
deleted file mode 100644
index 3a42151..0000000
--- a/tests/reflection_test.py
+++ /dev/null
@@ -1,787 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-from . import TestCase, SkipTest
-import os
-
-import testdata
-
-from endpoints.call import Controller
-from endpoints.reflection import (
-    Reflect,
-    ReflectMethod,
-    ReflectController,
-    ReflectModule,
-    ReflectPath,
-    ReflectHTTPMethod,
-    ReflectClass,
-)
-
-
-class ReflectTest(TestCase):
-
-    def create_reflect(self, controller_prefix):
-        return Reflect([controller_prefix])
-
-    def find_reflect(self, uri, controllers):
-        ret = None
-        for d in controllers:
-            if d.uri == uri:
-                ret = d
-                break
-        return ret
-
-    def test_controllers(self):
-        # putting the C back in CRUD
-        controller_prefix = "controller_reflect_endpoints"
-        testdata.create_modules({
-            controller_prefix: [
-                "import endpoints",
-                "class Default(endpoints.Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.foo".format(controller_prefix): [
-                "import endpoints",
-                "class Default(endpoints.Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.che".format(controller_prefix): [
-                "from endpoints import Controller",
-                "class Baz(Controller):",
-                "    def POST(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.che.bam".format(controller_prefix): [
-                "from endpoints import Controller as Con",
-                "class _Base(Con):",
-                "    def GET(*args, **kwargs): pass",
-                "",
-                "class Boo(_Base):",
-                "    def DELETE(*args, **kwargs): pass",
-                "    def POST(*args, **kwargs): pass",
-                ""
-                "class Bah(_Base):",
-                "    '''this is the doc string'''",
-                "    def HEAD(*args, **kwargs): pass",
-                ""
-            ]
-        })
-
-        r = self.create_reflect(controller_prefix)
-        l = list(r.controllers)
-        self.assertEqual(5, len(l))
-
-        d = self.find_reflect("/che/bam/bah", l)
-        self.assertSetEqual(set(["GET", "HEAD", "OPTIONS"]), set(d.methods.keys()))
-        self.assertGreater(len(d.desc), 0)
-
-        d = self.find_reflect("/", l)
-        self.assertNotEqual(d, None)
-
-        d = self.find_reflect("/foo", l)
-        self.assertNotEqual(d, None)
-
-    def test_methods(self):
-        class MethodsController(Controller):
-            def POST(self): pass
-            def GET(self): pass
-            def ABSURD(self): pass
-            def ignORED(self): pass
-
-        rc = ReflectController(None, MethodsController, "foo")
-        methods = list(rc.methods)
-
-        self.assertEqual(4, len(methods))
-        for o in ['ABSURD', 'GET', 'POST', 'OPTIONS']:
-            self.assertTrue(o in methods)
-
-    def test_versioned_controllers(self):
-        # putting the C back in CRUD
-        controller_prefix = "versioned_controllers"
-        testdata.create_modules({
-                "foo": [
-                    "import endpoints",
-                    "from endpoints.decorators import param, version",
-                    "class Bar(endpoints.Controller):",
-                    "    @param('foo', default=1, type=int)",
-                    "    @param('bar', type=bool, required=False)",
-                    "    @version('v1')",
-                    "    def GET_v1(self): pass",
-                    "",
-                    "    @version('v2')",
-                    "    def GET_v2(self): pass",
-                    ""
-                ],
-                "che": [
-                    "from endpoints import Controller",
-                    "from endpoints.decorators import version",
-                    "class Baz(Controller):",
-                    "    @version('v3')",
-                    "    def GET_v3(self): pass",
-                    ""
-                ],
-            }, prefix=controller_prefix)
-
-        rs = self.create_reflect(controller_prefix)
-        l = list(rs.controllers)
-
-        self.assertEqual(2, len(l))
-        for d in l:
-            self.assertEqual(2, len(d.methods))
-
-        d = self.find_reflect("/foo/bar", l)
-        self.assertTrue(d)
-
-        d = self.find_reflect("/che/baz", l)
-        self.assertTrue(d)
-
-    def test_decorators_inherit_2(self):
-        """you have a parent class with POST method, the child also has a POST method,
-        what do you do? What. Do. You. Do?"""
-        controller_prefix = "decinherit2"
-        m = testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "",
-            "def a(f):",
-            "    def wrapped(*args, **kwargs):",
-            "        return f(*args, **kwargs)",
-            "    return wrapped",
-            "",
-            "class b(object):",
-            "    def __init__(self, func):",
-            "        self.func = func",
-            "    def __call__(*args, **kwargs):",
-            "        return f(*args, **kwargs)",
-            "",
-            "def c(func):",
-            "    def wrapper(*args, **kwargs):",
-            "        return func(*args, **kwargs)",
-            "    return wrapper",
-            "",
-            "def POST(): pass",
-            "",
-            "class D(object):",
-            "    def HEAD(): pass"
-            "",
-            "class _BaseController(endpoints.Controller):",
-            "    @a",
-            "    @b",
-            "    def POST(self, **kwargs): pass",
-            "",
-            "    @a",
-            "    @b",
-            "    def HEAD(self): pass",
-            "",
-            "    @a",
-            "    @b",
-            "    def GET(self): pass",
-            "",
-            "class Default(_BaseController):",
-            "    @c",
-            "    def POST(self, **kwargs): POST()",
-            "",
-            "    @c",
-            "    def HEAD(self):",
-            "        d = D()",
-            "        d.HEAD()",
-            "",
-            "    @c",
-            "    def GET(self):",
-            "        super(Default, self).GET()",
-            "",
-        ])
-
-        rs = self.create_reflect(controller_prefix)
-        l = list(rs.controllers)
-        r = l[0]
-        self.assertEqual(1, len(r.decorators["POST"]))
-        self.assertEqual(1, len(r.decorators["HEAD"]))
-        self.assertEqual(3, len(r.decorators["GET"]))
-
-
-    def test_decorator_inherit_1(self):
-        """make sure that a child class that hasn't defined a METHOD inherits the
-        METHOD method from its parent with decorators in tact"""
-        controller_prefix = "foodecinherit"
-        m = testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "",
-            "def foodec(func):",
-            "    def wrapper(*args, **kwargs):",
-            "        return func(*args, **kwargs)",
-            "    return wrapper",
-            "",
-            "class _BaseController(endpoints.Controller):",
-            "    @foodec",
-            "    def POST(self, **kwargs):",
-            "        return 1",
-            "",
-            "class Default(_BaseController):",
-            "    pass",
-            "",
-        ])
-        rs = self.create_reflect(controller_prefix)
-        for count, endpoint in enumerate(rs, 1):
-            self.assertEqual("foodec", endpoint.decorators["POST"][0].name)
-        self.assertEqual(1, count)
-
-    def test_docblock(self):
-        controller_prefix = "docblock"
-        testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "class Foo(endpoints.Controller):",
-            "    '''this is a multiline docblock",
-            "",
-            "    this means it has...",
-            "    ",
-            "    multiple lines",
-            "    '''",
-            "    def GET(*args, **kwargs): pass",
-            "",
-        ])
-
-        rs = self.create_reflect(controller_prefix)
-        for endpoint in rs:
-            self.assertTrue("\n" in endpoint.desc)
-
-    def test_method_docblock(self):
-        controller_prefix = "mdoc"
-        testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "class Foo(endpoints.Controller):",
-            "    '''controller docblock'''",
-            "    def GET(*args, **kwargs):",
-            "        '''method docblock'''",
-            "        pass",
-            "",
-        ])
-
-        rs = self.create_reflect(controller_prefix)
-        for endpoint in rs:
-            desc = endpoint.methods['GET'][0].desc
-            self.assertEqual("method docblock", desc)
- 
-    def test_method_docblock_bad_decorator(self):
-        tmpdir = testdata.create_dir("reflectdoc2")
-        controller_prefix = "mdoc2"
-        testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "",
-            "def bad_dec(func):",
-            "    def wrapper(*args, **kwargs):",
-            "        return func(*args, **kwargs)",
-            "    return wrapper",
-            "",
-            "class Foo(endpoints.Controller):",
-            "    '''controller docblock'''",
-            "    @bad_dec",
-            "    def GET(*args, **kwargs):",
-            "        '''method docblock'''",
-            "        pass",
-            "",
-            "    def POST(*args, **kwargs):",
-            "        '''should not return this docblock'''",
-            "        pass",
-            "",
-        ])
-
-        rs = self.create_reflect(controller_prefix)
-        for endpoint in rs:
-            desc = endpoint.methods['GET'][0].desc
-            self.assertEqual("method docblock", desc)
- 
-    def test_decorators(self):
-        controller_prefix = "controller_reflect"
-        testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "from endpoints.decorators import param",
-            "",
-            "def dec_func(f):",
-            "    def wrapped(*args, **kwargs):",
-            "        return f(*args, **kwargs)",
-            "    return wrapped",
-            "",
-            "class dec_cls(object):",
-            "    def __init__(self, func):",
-            "        self.func = func",
-            "    def __call__(*args, **kwargs):",
-            "        return f(*args, **kwargs)",
-            "",
-            "class Foo(endpoints.Controller):",
-            "    @dec_func",
-            "    def GET(*args, **kwargs): pass",
-            "    @dec_cls",
-            "    @param('foo', default=1, type=int)",
-            "    @param('bar', type=bool, required=False)",
-            "    @param('che_empty', type=dict, default={})",
-            "    @param('che_full', type=dict, default={'key': 'val', 'key2': 2.0})",
-            "    @param('baz_empty', type=list, default=[])",
-            "    @param('baz_full', type=list, default=['val', False, 1])",
-            "    @param('d')",
-            "    def POST(*args, **kwargs): pass",
-            ""
-        ])
-
-        rs = self.create_reflect(controller_prefix)
-        l = list(rs.controllers)
-        r = l[0]
-
-        methods = r.methods
-        params = methods['POST'][0].params
-        for p in ['d']:
-            self.assertTrue(params[p]['required'])
-
-        for p in ['foo', 'bar', 'che_empty', 'che_full', 'baz_empty', 'baz_full']:
-            self.assertFalse(params[p]['required'])
-
-        self.assertEqual(1, len(l))
-        self.assertEqual('/foo', r.uri)
-        self.assertSetEqual(set(['GET', 'POST', 'OPTIONS']), set(r.methods.keys()))
-
-    def test_decorators_param_help(self):
-        controller_prefix = "dec_param_help"
-        testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "from endpoints.decorators import param",
-            "class Default(endpoints.Controller):",
-            "    @param('baz_full', type=list, default=['val', False, 1], help='baz_full')",
-            "    @param('d', help='d')",
-            "    def POST(*args, **kwargs): pass",
-            ""
-        ])
-
-        rs = self.create_reflect(controller_prefix)
-        l = list(rs.controllers)
-        r = l[0]
-
-        methods = r.methods
-        params = methods['POST'][0].params
-        for k, v in params.items():
-            self.assertEqual(k, v['options']['help'])
-
-
-class ReflectControllerTest(TestCase):
-    def test_method_required_args(self):
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    @param(0)",
-            "    @param('one')",
-            "    def POST(self, *args, **kwargs): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rm = ReflectHTTPMethod("POST", mp.module.Foo.POST, ReflectController(rmod, mp.module.Foo, mp))
-        r = rm.required_args
-        self.assertEqual([0], r)
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    @param(0)",
-            "    @param('one')",
-            "    def POST(self, zero, one): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rm = ReflectHTTPMethod("POST", mp.module.Foo.POST, ReflectController(rmod, mp.module.Foo, mp))
-        r = rm.required_args
-        self.assertEqual(["zero"], r)
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    @param(0)",
-            "    @param(1, default=1)",
-            "    def POST(self, *args): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rm = ReflectHTTPMethod("POST", mp.module.Foo.POST, ReflectController(rmod, mp.module.Foo, mp))
-        r = rm.required_args
-        self.assertEqual([0], r)
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    @param(0)",
-            "    @param(1, default=1)",
-            "    def POST(self, zero, *args): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rm = ReflectHTTPMethod("POST", mp.module.Foo.POST, ReflectController(rmod, mp.module.Foo, mp))
-        r = rm.required_args
-        self.assertEqual(["zero"], r)
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    @param(0)",
-            "    @param(1, default=1)",
-            "    def POST(self, zero, one): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rm = ReflectHTTPMethod("POST", mp.module.Foo.POST, ReflectController(rmod, mp.module.Foo, mp))
-        r = rm.required_args
-        self.assertEqual(["zero"], r)
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    @param(0, default=0)",
-            "    @param(1, default=1)",
-            "    def POST(self, zero, one): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rm = ReflectHTTPMethod("POST", mp.module.Foo.POST, ReflectController(rmod, mp.module.Foo, mp))
-        r = rm.required_args
-        self.assertEqual([], r)
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    def POST(self, one, two=2, three=3): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rm = ReflectHTTPMethod("POST", mp.module.Foo.POST, ReflectController(rmod, mp.module.Foo, mp))
-        r = rm.required_args
-        self.assertEqual(1, len(r))
-
-    def test__get_methods_info(self):
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Bar(Controller):",
-            "    @version('v1')",
-            "    def GET_v1(self, **kwargs): pass",
-            "",
-            "    @version('v2')",
-            "    def GET_v2(self, *args): pass",
-            ""
-            "    def POST(self, *args, **kwargs): pass",
-        ])
-
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Bar, mp)
-        r = rc._get_methods_info()
-        self.assertEqual(2, len(r["GET"]))
-        self.assertEqual(1, len(r["POST"]))
-
-    def test_get_info(self):
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    def POST(self, one, two=2, three=3): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Foo, mp)
-        r = rc.get_info()
-        self.assertFalse(r["POST"]["POST"]["positionals"])
-        self.assertFalse(r["POST"]["POST"]["keywords"])
-        self.assertEqual(3, len(r["POST"]["POST"]["params"]))
-        self.assertEqual(3, r["POST"]["POST"]["params"][2]["default"])
-        self.assertTrue(r["POST"]["POST"]["params"][0]["required"])
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    @param(0, default=1)",
-            "    @param(1)",
-            "    @param('che')",
-            "    def POST(self, zero, one, che): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Foo, mp)
-        r = rc.get_info()
-        self.assertEqual(3, len(r["POST"]["POST"]["params"]))
-        self.assertEqual(3, len(r["POST"]["POST"]["decorators"]))
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    def GET(self, *args, **kwargs): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Foo, mp)
-        r = rc.get_info()
-        self.assertTrue(r["GET"]["GET"]["positionals"])
-        self.assertTrue(r["GET"]["GET"]["keywords"])
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    def GET(self): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Foo, mp)
-        r = rc.get_info()
-        self.assertFalse(r["GET"]["GET"]["positionals"])
-        self.assertFalse(r["GET"]["GET"]["keywords"])
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    def GET(self, **kwargs): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Foo, mp)
-        r = rc.get_info()
-        self.assertFalse(r["GET"]["GET"]["positionals"])
-        self.assertTrue(r["GET"]["GET"]["keywords"])
-
-        mp = testdata.create_module(contents=[
-            "from endpoints import param, version, Controller",
-            "class Foo(Controller):",
-            "    def GET(self, *args): pass",
-        ])
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Foo, mp)
-        r = rc.get_info()
-        self.assertTrue(r["GET"]["GET"]["positionals"])
-        self.assertFalse(r["GET"]["GET"]["keywords"])
-
-        # TODO this is a valid py3 method declarations:
-        # def foo(*args, bar, che): pass
-
-#         mp = testdata.create_module(controller_prefix, [
-#             "import endpoints",
-#             "from endpoints.decorators import param, version",
-#             "class Bar(endpoints.Controller):",
-#             "    @param('foo', default=1, type=int)",
-#             "    @version('v1')",
-#             "    def GET_v1(self, **kwargs): pass",
-#             "",
-#             "    @version('v2')",
-#             "    def GET_v2(self, *args): pass",
-#             ""
-#             "    @version('v3')",
-#             "    def GET_v3(self): pass",
-#             ""
-#             "    @param(0, default=1)",
-#             "    @param('che')",
-#             "    @version('v1')",
-#             "    def POST_v1(self, *args, **kwargs): pass",
-#             "",
-#             "    @param(0, default=1)",
-#             "    @param(1)",
-#             "    @param('che')",
-#             "    @version('v2')",
-#             "    def POST_v2(self, zero, one, che): pass",
-#         ])
-# 
-#         rc = ReflectController(controller_prefix, mp.module.Bar)
-#         rc._get_info()
-
-    def test_multi_methods(self):
-        controller_prefix = "multi_methods"
-        mp = testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "from endpoints.decorators import param, version",
-            "class Bar(endpoints.Controller):",
-            "    @param('foo', default=1, type=int)",
-            "    @param('bar', type=bool, required=False)",
-            "    @version('v1')",
-            "    def GET_version1(self): pass",
-            "",
-            "    @version('v2')",
-            "    def GET_version2(self): pass",
-            ""
-        ])
- 
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Bar, controller_prefix)
-        self.assertEqual(2, len(rc.methods["GET"]))
-
-        for rm in rc.methods["GET"]:
-            self.assertTrue(rm.version in ["v1", "v2"])
-
-    def test_metavar(self):
-        # https://github.com/firstopinion/endpoints/issues/58
-        controller_prefix = "metavar_rct"
-        mp = testdata.create_module(controller_prefix, [
-            "import endpoints",
-            "from endpoints.decorators import param, version",
-            "class Bar(endpoints.Controller):",
-            "    @param(0, metavar='bar')",
-            "    @param(1, metavar='che')",
-            "    def GET(self, bar, che): pass",
-            "",
-        ])
-
-        rmod = ReflectModule(mp)
-        rc = ReflectController(rmod, mp.module.Bar, controller_prefix)
-
-        for rm in rc.methods["GET"]:
-            for name, pr in rm.params.items():
-                self.assertTrue("metavar" in pr["options"])
-
-
-class ReflectModuleTest(TestCase):
-    def test_mixed_modules_packages(self):
-        """make sure a package with modules and other packages will resolve correctly"""
-        controller_prefix = "mmp"
-        r = testdata.create_modules({
-            "": [
-                "from endpoints import Controller",
-                "class Default(Controller): pass",
-            ],
-            "foo": [
-                "from endpoints import Controller",
-                "class Default(Controller): pass",
-            ],
-            "foo.bar": [
-                "from endpoints import Controller",
-                "class Default(Controller): pass",
-            ],
-            "che": [
-                "from endpoints import Controller",
-                "class Default(Controller): pass",
-            ],
-        }, prefix=controller_prefix)
-
-        r = ReflectModule(controller_prefix)
-        self.assertEqual(set(['mmp.foo', 'mmp', 'mmp.foo.bar', 'mmp.che']), r.module_names)
-
-        # make sure just a file will resolve correctly
-        controller_prefix = "mmp2"
-        testdata.create_module(controller_prefix, os.linesep.join([
-            "from endpoints import Controller",
-            "class Bar(Controller): pass",
-        ]))
-        r = ReflectModule(controller_prefix)
-        self.assertEqual(set(['mmp2']), r.module_names)
-
-    def test_routing_module(self):
-        controller_prefix = "routing_module"
-        contents = [
-            "from endpoints import Controller",
-            "class Bar(Controller):",
-            "    def GET(*args, **kwargs): pass"
-        ]
-        testdata.create_module("{}.foo".format(controller_prefix), contents=contents)
-
-        r = ReflectModule(controller_prefix)
-        self.assertTrue(controller_prefix in r.module_names)
-        self.assertEqual(2, len(r.module_names))
-
-    def test_routing_package(self):
-        controller_prefix = "routepack"
-        contents = [
-            "from endpoints import Controller",
-            "",
-            "class Default(Controller):",
-            "    def GET(self): pass",
-            "",
-        ]
-        f = testdata.create_package(controller_prefix, contents=contents)
-
-        r = ReflectModule(controller_prefix)
-        self.assertTrue(controller_prefix in r.module_names)
-        self.assertEqual(1, len(r.module_names))
-
-    def test_controllers(self):
-        controller_prefix = "get_controllers"
-        d = {
-            controller_prefix: [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.default".format(controller_prefix): [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.foo".format(controller_prefix): [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                "",
-                "class Bar(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                "    def POST(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.foo.baz".format(controller_prefix): [
-                "from endpoints import Controller",
-                "class Default(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                "",
-                "class Che(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-            "{}.foo.boom".format(controller_prefix): [
-                "from endpoints import Controller",
-                "",
-                "class Bang(Controller):",
-                "    def GET(*args, **kwargs): pass",
-                ""
-            ],
-        }
-        r = testdata.create_modules(d)
-        s = set(d.keys())
-
-
-        r = ReflectModule(controller_prefix)
-        controllers = r.module_names
-        self.assertEqual(s, controllers)
-
-        # just making sure it always returns the same list
-        controllers = r.module_names
-        self.assertEqual(s, controllers)
-
-
-class ReflectPathTest(TestCase):
-    def test_modules(self):
-        prefix = "reflectmodules"
-        path = testdata.create_modules({
-            "foo": [
-                "class Foo(object): pass"
-            ],
-            "foo.bar": [
-                "class Bar1(object): pass",
-                "class Bar2(object): pass"
-            ],
-            "che": [
-                "class Che(object): pass"
-            ],
-        }, prefix=prefix)
-
-        r = ReflectPath(path)
-        s = set([
-            prefix,
-            "{}.foo".format(prefix),
-            "{}.foo.bar".format(prefix),
-            "{}.che".format(prefix)
-        ])
-        self.assertEqual(s, r.module_names)
-
-    def test_path_is_package(self):
-        m = testdata.create_package(contents="class Foo(object): pass")
-
-        r = ReflectPath(m.directory)
-        # we don't return the path because it isn't importable by name from the path we
-        # passed in (itself), as of 7-4-2019 I think this is the correct behavior
-        self.assertEqual(set(), r.module_names)
-
-    def test_find_modules(self):
-        prefix = "findmodules"
-        path = testdata.create_modules({
-            "foo": [
-                "class Foo(object): pass"
-            ],
-            "foo.bar": [
-                "class Bar1(object): pass",
-                "class Bar2(object): pass"
-            ],
-            "che": [
-                "class Che(object): pass"
-            ],
-        }, prefix=prefix)
-
-        r = ReflectPath(path)
-        ms = list(r.find_modules(lambda rm: rm.module_name.endswith("foo")))
-        self.assertEqual(1, len(ms))
-        self.assertEqual("{}.foo".format(prefix), ms[0].module_name)
-
-
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 2a6e87c..6deda19 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -1,12 +1,16 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-from unittest import TestCase
 import json
 
-import testdata
-
 from endpoints.compat import *
-from endpoints.utils import MimeType, AcceptHeader, JSONEncoder
+from endpoints.utils import (
+    MimeType,
+    AcceptHeader,
+    JSONEncoder,
+    Url,
+    Status,
+)
+
+from . import TestCase
 
 
 class MimeTypeTest(TestCase):
@@ -30,7 +34,6 @@ def test_default_file(self):
 
 
 class AcceptHeaderTest(TestCase):
-
     def test_init(self):
         ts = [
             (
@@ -43,7 +46,13 @@ def test_init(self):
                 ]
             ),
             (
-                'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5',
+                ", ".join([
+                    "text/*;q=0.3",
+                    "text/html;q=0.7",
+                    "text/html;level=1",
+                    "text/html;level=2;q=0.4",
+                    "*/*;q=0.5",
+                ]),
                 [
                     "text/html;level=1",
                     "text/html;q=0.7",
@@ -107,11 +116,6 @@ def test_filter(self):
 
             self.assertEqual(t[2], count)
 
-#     def test_bad_split(self):
-#         a = AcceptHeader("application/json;version=v1")
-#         for mt in a.filter("application/json"):
-#             pout.v(mt)
-
 
 class JSONEncoderTest(TestCase):
     def test_string(self):
@@ -119,3 +123,42 @@ def test_string(self):
         r2 = json.dumps({'foo': 'bar'}, cls=JSONEncoder)
         self.assertEqual(r1, r2)
 
+
+class UrlTest(TestCase):
+    def test_module(self):
+        c = self.create_server({
+            "foo": [
+                "from endpoints import Controller",
+                "class Bar(Controller):",
+                "    def GET(self):",
+                "        u = self.request.url",
+                "        return u.module()",
+                "",
+                "class Default(Controller):",
+                "    def GET(self):",
+                "        u = self.request.url",
+                "        return u.module()",
+                "",
+            ],
+        })
+
+        res = c.handle("/foo/bar")
+        self.assertEqual("http://endpoints.fake/foo", res._body)
+
+        res = c.handle("/foo")
+        self.assertEqual("http://endpoints.fake/foo", res._body)
+
+    def test_controller(self):
+        u = Url("http://example.com/foo/bar/che", class_path="foo")
+        u2 = u.controller(che=4)
+        self.assertEqual("http://example.com/foo?che=4", u2)
+
+
+class StatusTest(TestCase):
+    def test_codes(self):
+        s = Status(401)
+        self.assertEqual("Unauthorized", s)
+
+        s = Status(1001)
+        self.assertEqual("Close Going Away", s)
+