Web Server Quickstart¶
Run a Simple Web Server¶
In order to implement a web server, first create a request handler.
A request handler must be a coroutine that
accepts a Request
instance as its only parameter and returns a
Response
instance:
from aiohttp import web
async def hello(request):
return web.Response(text="Hello, world")
Next, create an Application
instance and register the
request handler on a particular HTTP method and path:
app = web.Application()
app.add_routes([web.get('/', hello)])
After that, run the application by run_app()
call:
web.run_app(app)
That’s it. Now, head over to http://localhost:8080/
to see the results.
Alternatively if you prefer route decorators create a route table and register a web-handler:
routes = web.RouteTableDef()
@routes.get('/')
async def hello(request):
return web.Response(text="Hello, world")
app = web.Application()
app.add_routes(routes)
web.run_app(app)
Both ways essentially do the same work, the difference is only in your
taste: do you prefer Django style with famous urls.py
or Flask
with shiny route decorators.
aiohttp server documentation uses both ways in code snippets to emphasize their equality, switching from one style to another is very trivial.
See also
Graceful shutdown section explains what run_app()
does and how to implement complex server initialization/finalization
from scratch.
Application runners for more handling more complex cases like asynchronous web application serving and multiple hosts support.
Command Line Interface (CLI)¶
aiohttp.web
implements a basic CLI for quickly serving an
Application
in development over TCP/IP:
$ python -m aiohttp.web -H localhost -P 8080 package.module:init_func
package.module:init_func
should be an importable callable that
accepts a list of any non-parsed command-line arguments and returns an
Application
instance after setting it up:
def init_func(argv):
app = web.Application()
app.router.add_get("/", index_handler)
return app
Handler¶
A request handler must be a coroutine that accepts a
Request
instance as its only argument and returns a
StreamResponse
derived (e.g. Response
) instance:
async def handler(request):
return web.Response()
Handlers are setup to handle requests by registering them with the
Application.add_routes()
on a particular route (HTTP method and
path pair) using helpers like get()
and
post()
:
app.add_routes([web.get('/', handler),
web.post('/post', post_handler),
web.put('/put', put_handler)])
Or use route decorators:
routes = web.RouteTableDef()
@routes.get('/')
async def get_handler(request):
...
@routes.post('/post')
async def post_handler(request):
...
@routes.put('/put')
async def put_handler(request):
...
app.add_routes(routes)
Wildcard HTTP method is also supported by route()
or
RouteTableDef.route()
, allowing a handler to serve incoming
requests on a path having any HTTP method:
app.add_routes([web.route('*', '/path', all_handler)])
The HTTP method can be queried later in the request handler using the
aiohttp.web.BaseRequest.method
property.
By default endpoints added with GET
method will accept
HEAD
requests and return the same response headers as they would
for a GET
request. You can also deny HEAD
requests on a route:
web.get('/', handler, allow_head=False)
Here handler
won’t be called on HEAD
request and the server
will respond with 405: Method Not Allowed
.
See also
Peer disconnection section explains how handlers behave when a client connection drops and ways to optimize handling of this.
Resources and Routes¶
Internally routes are served by Application.router
(UrlDispatcher
instance).
The router is a list of resources.
Resource is an entry in route table which corresponds to requested URL.
Resource in turn has at least one route.
Route corresponds to handling HTTP method by calling web handler.
Thus when you add a route the resource object is created under the hood.
The library implementation merges all subsequent route additions for the same path adding the only resource for all HTTP methods.
Consider two examples:
app.add_routes([web.get('/path1', get_1),
web.post('/path1', post_1),
web.get('/path2', get_2),
web.post('/path2', post_2)]
and:
app.add_routes([web.get('/path1', get_1),
web.get('/path2', get_2),
web.post('/path2', post_2),
web.post('/path1', post_1)]
First one is optimized. You have got the idea.
Variable Resources¶
Resource may have variable path also. For instance, a resource with
the path '/a/{name}/c'
would match all incoming requests with
paths such as '/a/b/c'
, '/a/1/c'
, and '/a/etc/c'
.
A variable part is specified in the form {identifier}
, where the
identifier
can be used later in a
request handler to access the matched value for
that part. This is done by looking up the identifier
in the
Request.match_info
mapping:
@routes.get('/{name}')
async def variable_handler(request):
return web.Response(
text="Hello, {}".format(request.match_info['name']))
By default, each part matches the regular expression [^{}/]+
.
You can also specify a custom regex in the form {identifier:regex}
:
web.get(r'/{name:\d+}', handler)
Reverse URL Constructing using Named Resources¶
Routes can also be given a name:
@routes.get('/root', name='root')
async def handler(request):
...
Which can then be used to access and build a URL for that resource later (e.g. in a request handler):
url = request.app.router['root'].url_for().with_query({"a": "b", "c": "d"})
assert url == URL('/root?a=b&c=d')
A more interesting example is building URLs for variable resources:
app.router.add_resource(r'/{user}/info', name='user-info')
In this case you can also pass in the parts of the route:
url = request.app.router['user-info'].url_for(user='john_doe')
url_with_qs = url.with_query("a=b")
assert url_with_qs == '/john_doe/info?a=b'
Organizing Handlers in Classes¶
As discussed above, handlers can be first-class coroutines:
async def hello(request):
return web.Response(text="Hello, world")
app.router.add_get('/', hello)
But sometimes it’s convenient to group logically similar handlers into a Python class.
Since aiohttp.web
does not dictate any implementation details,
application developers can organize handlers in classes if they so wish:
class Handler:
def __init__(self):
pass
async def handle_intro(self, request):
return web.Response(text="Hello, world")
async def handle_greeting(self, request):
name = request.match_info.get('name', "Anonymous")
txt = "Hello, {}".format(name)
return web.Response(text=txt)
handler = Handler()
app.add_routes([web.get('/intro', handler.handle_intro),
web.get('/greet/{name}', handler.handle_greeting)])
Class Based Views¶
aiohttp.web
has support for class based views.
You can derive from View
and define methods for handling http
requests:
class MyView(web.View):
async def get(self):
return await get_resp(self.request)
async def post(self):
return await post_resp(self.request)
Handlers should be coroutines accepting self only and returning
response object as regular web-handler. Request object can be
retrieved by View.request
property.
After implementing the view (MyView
from example above) should be
registered in application’s router:
web.view('/path/to', MyView)
or:
@routes.view('/path/to')
class MyView(web.View):
...
Example will process GET and POST requests for /path/to but raise 405 Method not allowed exception for unimplemented HTTP methods.
Resource Views¶
All registered resources in a router can be viewed using the
UrlDispatcher.resources()
method:
for resource in app.router.resources():
print(resource)
A subset of the resources that were registered with a name can be
viewed using the UrlDispatcher.named_resources()
method:
for name, resource in app.router.named_resources().items():
print(name, resource)
Alternative ways for registering routes¶
Code examples shown above use imperative style for adding new
routes: they call app.router.add_get(...)
etc.
There are two alternatives: route tables and route decorators.
Route tables look like Django way:
async def handle_get(request):
...
async def handle_post(request):
...
app.router.add_routes([web.get('/get', handle_get),
web.post('/post', handle_post),
The snippet calls add_routes()
to
register a list of route definitions (aiohttp.web.RouteDef
instances) created by aiohttp.web.get()
or
aiohttp.web.post()
functions.
See also
RouteDef and StaticDef reference.
Route decorators are closer to Flask approach:
routes = web.RouteTableDef()
@routes.get('/get')
async def handle_get(request):
...
@routes.post('/post')
async def handle_post(request):
...
app.router.add_routes(routes)
It is also possible to use decorators with class-based views:
routes = web.RouteTableDef()
@routes.view("/view")
class MyView(web.View):
async def get(self):
...
async def post(self):
...
app.router.add_routes(routes)
The example creates a aiohttp.web.RouteTableDef
container first.
The container is a list-like object with additional decorators
aiohttp.web.RouteTableDef.get()
,
aiohttp.web.RouteTableDef.post()
etc. for registering new
routes.
After filling the container
add_routes()
is used for adding
registered route definitions into application’s router.
See also
RouteTableDef reference.
All tree ways (imperative calls, route tables and decorators) are equivalent, you could use what do you prefer or even mix them on your own.
Added in version 2.3.
JSON Response¶
It is a common case to return JSON data in response, aiohttp.web
provides a shortcut for returning JSON – aiohttp.web.json_response()
:
async def handler(request):
data = {'some': 'data'}
return web.json_response(data)
The shortcut method returns aiohttp.web.Response
instance
so you can for example set cookies before returning it from handler.
User Sessions¶
Often you need a container for storing user data across requests. The concept is usually called a session.
aiohttp.web
has no built-in concept of a session, however, there is a
third-party library, aiohttp_session
, that adds session support:
import asyncio
import time
import base64
from cryptography import fernet
from aiohttp import web
from aiohttp_session import setup, get_session, session_middleware
from aiohttp_session.cookie_storage import EncryptedCookieStorage
async def handler(request):
session = await get_session(request)
last_visit = session.get("last_visit")
session["last_visit"] = time.time()
text = "Last visited: {}".format(last_visit)
return web.Response(text=text)
async def make_app():
app = web.Application()
# secret_key must be 32 url-safe base64-encoded bytes
fernet_key = fernet.Fernet.generate_key()
secret_key = base64.urlsafe_b64decode(fernet_key)
setup(app, EncryptedCookieStorage(secret_key))
app.add_routes([web.get('/', handler)])
return app
web.run_app(make_app())
HTTP Forms¶
HTTP Forms are supported out of the box.
If form’s method is "GET"
(<form method="get">
) use
aiohttp.web.BaseRequest.query
for getting form data.
To access form data with "POST"
method use
aiohttp.web.BaseRequest.post()
or aiohttp.web.BaseRequest.multipart()
.
aiohttp.web.BaseRequest.post()
accepts both
'application/x-www-form-urlencoded'
and 'multipart/form-data'
form’s data encoding (e.g. <form enctype="multipart/form-data">
).
It stores files data in temporary directory. If client_max_size is
specified post raises ValueError exception.
For efficiency use aiohttp.web.BaseRequest.multipart()
, It is especially effective
for uploading large files (File Uploads).
Values submitted by the following form:
<form action="/login" method="post" accept-charset="utf-8"
enctype="application/x-www-form-urlencoded">
<label for="login">Login</label>
<input id="login" name="login" type="text" value="" autofocus/>
<label for="password">Password</label>
<input id="password" name="password" type="password" value=""/>
<input type="submit" value="login"/>
</form>
could be accessed as:
async def do_login(request):
data = await request.post()
login = data['login']
password = data['password']
File Uploads¶
aiohttp.web
has built-in support for handling files uploaded from the
browser.
First, make sure that the HTML <form>
element has its enctype attribute
set to enctype="multipart/form-data"
. As an example, here is a form that
accepts an MP3 file:
<form action="/store/mp3" method="post" accept-charset="utf-8"
enctype="multipart/form-data">
<label for="mp3">Mp3</label>
<input id="mp3" name="mp3" type="file" value=""/>
<input type="submit" value="submit"/>
</form>
Then, in the request handler you can access the
file input field as a FileField
instance. FileField
is simply
a container for the file as well as some of its metadata:
async def store_mp3_handler(request):
# WARNING: don't do that if you plan to receive large files!
data = await request.post()
mp3 = data['mp3']
# .filename contains the name of the file in string format.
filename = mp3.filename
# .file contains the actual file data that needs to be stored somewhere.
mp3_file = data['mp3'].file
content = mp3_file.read()
return web.Response(body=content,
headers=MultiDict(
{'CONTENT-DISPOSITION': mp3_file}))
You might have noticed a big warning in the example above. The general issue is
that aiohttp.web.BaseRequest.post()
reads the whole payload in memory,
resulting in possible
OOM errors. To avoid this, for multipart uploads, you
should use aiohttp.web.BaseRequest.multipart()
which returns a multipart reader:
async def store_mp3_handler(request):
reader = await request.multipart()
# /!\ Don't forget to validate your inputs /!\
# reader.next() will `yield` the fields of your form
field = await reader.next()
assert field.name == 'name'
name = await field.read(decode=True)
field = await reader.next()
assert field.name == 'mp3'
filename = field.filename
# You cannot rely on Content-Length if transfer is chunked.
size = 0
with open(os.path.join('/spool/yarrr-media/mp3/', filename), 'wb') as f:
while True:
chunk = await field.read_chunk() # 8192 bytes by default.
if not chunk:
break
size += len(chunk)
f.write(chunk)
return web.Response(text='{} sized of {} successfully stored'
''.format(filename, size))
WebSockets¶
aiohttp.web
supports WebSockets out-of-the-box.
To setup a WebSocket, create a WebSocketResponse
in a
request handler and then use it to communicate
with the peer:
async def websocket_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
if msg.data == 'close':
await ws.close()
else:
await ws.send_str(msg.data + '/answer')
elif msg.type == aiohttp.WSMsgType.ERROR:
print('ws connection closed with exception %s' %
ws.exception())
print('websocket connection closed')
return ws
The handler should be registered as HTTP GET processor:
app.add_routes([web.get('/ws', websocket_handler)])
Redirects¶
To redirect user to another endpoint - raise HTTPFound
with
an absolute URL, relative URL or view name (the argument from router):
raise web.HTTPFound('/redirect')
The following example shows redirect to view named ‘login’ in routes:
async def handler(request):
location = request.app.router['login'].url_for()
raise web.HTTPFound(location=location)
router.add_get('/handler', handler)
router.add_get('/login', login_handler, name='login')
Example with login validation:
@aiohttp_jinja2.template('login.html')
async def login(request):
if request.method == 'POST':
form = await request.post()
error = validate_login(form)
if error:
return {'error': error}
else:
# login form is valid
location = request.app.router['index'].url_for()
raise web.HTTPFound(location=location)
return {}
app.router.add_get('/', index, name='index')
app.router.add_get('/login', login, name='login')
app.router.add_post('/login', login, name='login')
Exceptions¶
aiohttp.web
defines a set of exceptions for every HTTP status code.
Each exception is a subclass of HTTPException
and relates to a single
HTTP status code:
async def handler(request):
raise aiohttp.web.HTTPFound('/redirect')
Warning
Returning HTTPException
or its subclasses is deprecated and will
be removed in subsequent aiohttp versions.
Each exception class has a status code according to RFC 2068: codes with 100-300 are not really errors; 400s are client errors, and 500s are server errors.
HTTP Exception hierarchy chart:
Exception
HTTPException
HTTPSuccessful
* 200 - HTTPOk
* 201 - HTTPCreated
* 202 - HTTPAccepted
* 203 - HTTPNonAuthoritativeInformation
* 204 - HTTPNoContent
* 205 - HTTPResetContent
* 206 - HTTPPartialContent
HTTPRedirection
* 300 - HTTPMultipleChoices
* 301 - HTTPMovedPermanently
* 302 - HTTPFound
* 303 - HTTPSeeOther
* 304 - HTTPNotModified
* 305 - HTTPUseProxy
* 307 - HTTPTemporaryRedirect
* 308 - HTTPPermanentRedirect
HTTPError
HTTPClientError
* 400 - HTTPBadRequest
* 401 - HTTPUnauthorized
* 402 - HTTPPaymentRequired
* 403 - HTTPForbidden
* 404 - HTTPNotFound
* 405 - HTTPMethodNotAllowed
* 406 - HTTPNotAcceptable
* 407 - HTTPProxyAuthenticationRequired
* 408 - HTTPRequestTimeout
* 409 - HTTPConflict
* 410 - HTTPGone
* 411 - HTTPLengthRequired
* 412 - HTTPPreconditionFailed
* 413 - HTTPRequestEntityTooLarge
* 414 - HTTPRequestURITooLong
* 415 - HTTPUnsupportedMediaType
* 416 - HTTPRequestRangeNotSatisfiable
* 417 - HTTPExpectationFailed
* 421 - HTTPMisdirectedRequest
* 422 - HTTPUnprocessableEntity
* 424 - HTTPFailedDependency
* 426 - HTTPUpgradeRequired
* 428 - HTTPPreconditionRequired
* 429 - HTTPTooManyRequests
* 431 - HTTPRequestHeaderFieldsTooLarge
* 451 - HTTPUnavailableForLegalReasons
HTTPServerError
* 500 - HTTPInternalServerError
* 501 - HTTPNotImplemented
* 502 - HTTPBadGateway
* 503 - HTTPServiceUnavailable
* 504 - HTTPGatewayTimeout
* 505 - HTTPVersionNotSupported
* 506 - HTTPVariantAlsoNegotiates
* 507 - HTTPInsufficientStorage
* 510 - HTTPNotExtended
* 511 - HTTPNetworkAuthenticationRequired
All HTTP exceptions have the same constructor signature:
HTTPNotFound(*, headers=None, reason=None,
body=None, text=None, content_type=None)
If not directly specified, headers will be added to the default response headers.
Classes HTTPMultipleChoices
, HTTPMovedPermanently
,
HTTPFound
, HTTPSeeOther
, HTTPUseProxy
,
HTTPTemporaryRedirect
have the following constructor signature:
HTTPFound(location, *, headers=None, reason=None,
body=None, text=None, content_type=None)
where location is value for Location HTTP header.
HTTPMethodNotAllowed
is constructed by providing the incoming
unsupported method and list of allowed methods:
HTTPMethodNotAllowed(method, allowed_methods, *,
headers=None, reason=None,
body=None, text=None, content_type=None)
HTTPUnavailableForLegalReasons
should be constructed with a link
to yourself (as the entity implementing the blockage), and an explanation for
the block included in text
.:
HTTPUnavailableForLegalReasons(link, *,
headers=None, reason=None,
body=None, text=None, content_type=None)