Skip to content

Returning Responses

Basic Responses

In any web framework, returning a response can be as simple as returning a string of text or quite complex with all sorts of things like server-side rendering. Right out of the box, View supports returning status codes, headers, and a response without any fancy tooling. A response must contain a body (this is a str or bytes), but may also contain a status (int) or headers (dict[str, str]). These may be in any order.

from view import new_app

app = new_app()

@app.get("/")
async def index():
    return "Hello, view.py", 201, {"x-my-header": "my_header"}

HTTP Errors

Generally when returning a client error or server error, you want to skip future execution. For example:

from view import new_app

app = new_app()

@app.get("/")
async def index(number: int):
    if number == 1:
        return "number cannot be one", 400

    return f"your number is {number}"

app.run()

However, manually returning can be messy. For this, view.py provides you the Error class, which behaves like an Exception. It takes two parameters:

  • The status code, which is 400 by default.
  • The message to send back to the user. If this is None, it uses the default error message (e.g. Bad Request for error 400).

Since Error works like a Python exception, you can raise it just fine:

from view import new_app, Error

app = new_app()

@app.get("/")
async def index(number: int):
    if number == 1:
        raise Error(400)

    return f"your number is {number}"

app.run()

Warning

Error can only be used to send back error responses. It can not be used to return status codes such as 200.

Caching

Sometimes, computing the response for a route can be expensive or unnecessary. For this, view.py, along with many other web frameworks, provide the ability to cache responses.

View lets you do this by using the cache_rate parameter on a router.

For example:

from view import new_app

app = new_app()

@app.get("/", cache_rate=10)  # reload this route every 10 requests
async def index():
    return "..."

app.run()

You can see this in more detail by using a route that changes it's responses:

from view import new_app

app = new_app()
count = 1

@app.get("/", cache_rate=10)
async def index():
    global count
    count += 1
    return str(count)

app.run()

In the above example, index is only called every 10 requests, so after 20 calls, count would be 2.

Response Protocol

If you have some sort of object that you want to wrap a response around, view.py gives you the __view_result__ protocol. The only requirements are:

  • __view_result__ is available on the returned object (doesn't matter if it's static or instance)
  • __view_result__ returns data that corresponds to the allowed return values.

For example, a type MyObject defining __view_result__ could look like:

from view import new_app

app = new_app()

class MyObject:
    def __view_result__(self):
        return "Hello from MyObject!", {"x-www-myobject": "foo"}

@app.get("/")
async def index():
    return MyObject()  # this is ok

app.run()

Note that in the above scenario, you wouldn't actually need a whole object. Instead, you could also just define a utility function:

def _response():
    return "Hello, view.py!", {"foo": "bar"}

@app.get("/")
async def index():
    return _response()

Response Objects

View comes with two built in response objects: Response and HTML.

  • Response is simply a wrapper around other responses.
  • HTML is for returning HTML content.
  • JSON is for returning JSON content.

A common use case for Response is wrapping an object that has a __view_result__ and changing one of the values. For example:

from view import new_app, Response

app = new_app()

class Test:
    def __view_result__(self):
        return "test", 201

@app.get("/")
async def index():
    return Response(Test(), status=200)  # 200 is returned, not 201

app.run()

Another common case for Response is using cookies. You can add a cookie to the response via the cookie method:

@app.get("/")
async def index():
    res = Response(...)
    res.cookie("hello", "world")
    return res

Note that all response classes inherit from Response, meaning you can use this functionality anywhere.

Note

A Response must be returned for things like cookie to take effect. For example:

from view import new_app, Response

app = new_app()

@app.get("/")
async def index():
    res = Response(...)
    return "..."  # res is not returned!

app.run()

Body Translate Strategy

The body translate strategy in the __view_result__ protocol refers to how the Response class will translate the body into a str. There are four available strategies:

  • str, which uses the object's __str__ method.
  • repr, which uses the object's __repr__ method.
  • result, which calls the __view_result__ protocol implemented on the object (assuming it exists).
  • custom, uses the Response instance's _custom attribute (this only works on subclasses of Response that implement it).

For example, the route below would return the string "'hi'":

from view import new_app, Response

app = new_app()

@app.get("/")
async def index():
    res = Response('hi', body_translate="repr")
    return res

app.run()

Implementing Responses

Response is a generic type, meaning you should supply it a type argument when writing a class that inherits from it.

For example, if you wanted to write a type that takes a str:

class MyResponse(Response[str]):
    def __init__(self, body: str) -> None:
        super().__init__(body)

Generally, you'll want to use the custom translation strategy when writing custom Response objects.

You must implement the _custom method (which takes in the T passed to Response, and returns a str) to use the custom strategy. For example, the code below would be for a Response type that formats a list:

from view import Response

class ListResponse(Response[list]):
    def __init__(self, body: list) -> None:
        super().__init__(body, body_translate="custom")

    def _custom(self, body: list) -> str:
        return " ".join(body)

Middleware

The Middleware API

Route.middleware is used to define a middleware function for a route. Like other web frameworks, middleware functions are given a call_next. Note that call_next is always asynchronous regardless of whether the route is asynchronous.

from view import new_app, CallNext

app = new_app()

@app.get("/")
def index():
    return "my response!"

@index.middleware
async def index_middleware(call_next: CallNext):
    print("this is called before index()!")
    res = await call_next()
    print("this is called after index()!")
    return res

app.run()

Response Parsing

As shown above, call_next returns the result of the route. However, dealing with the raw response tuple might be a bit of a hassle. Instead, you can convert the response to a Response object using the to_response function:

from view import new_app, CallNext, to_response
from time import perf_counter

app = new_app()

@app.get("/")
def index():
    return "my response!"

@index.middleware
async def took_time_middleware(call_next: CallNext):
    a = perf_counter()
    res = to_response(await call_next())
    b = perf_counter()
    res.headers["X-Time-Elapsed"] = str(b - a)
    return res

app.run()

Review

Responses can be returned with a string, integer, and/or dictionary in any order.

  • The string represents the body of the response (e.g. HTML or JSON)
  • The integer represents the status code (200 by default)
  • The dictionary represents the headers (e.g. {"x-www-my-header": "some value"})

Response objects can also be returned, which implement the __view_result__ protocol. All response classes inherit from Response, which supports operations like setting cookies.

Finally, the middleware method on a Route can be used to implement middleware.