Asynchronous HTTP Service
The HTTP asynchronous service is an enhanced version of the HTTP service, based on aiohttp. It supports both synchronous and asynchronous servlets, keeping compatibility with the existing HTTP service.
Important
The HTTP asynchronous service implementation requires the aiohttp package to be installed.
For example, use: pip install aiohttp to install it.
The asynchronous HTTP service adds support for asynchronous servlets, allowing the implementation of long-running operations like Server Sent Events and Websockets.
Configuration properties
The asynchronous HTTP service supports the same properties as the basic HTTP service:
Property |
Default |
Description |
|---|---|---|
pelix.http.address |
0.0.0.0 |
The address the HTTP server is bound to |
pelix.http.port |
8080 |
The port the HTTP server is bound to |
Instantiation
The asynchronous HTTP service factory provided by the pelix.http.basic_async
bundle is named pelix.http.service.async.factory.
Here is a snippet that starts a HTTP server component, named http-server,
which only accepts local clients on port 9000:
from pelix.framework import FrameworkFactory
from pelix.ipopo.constants import use_ipopo
# Start the framework
framework = FrameworkFactory.get_framework()
framework.start()
context = framework.get_bundle_context()
# Install & start iPOPO
context.install_bundle('pelix.ipopo.core').start()
# Install & start the basic HTTP service
context.install_bundle('pelix.http.basic_async').start()
# Instantiate a HTTP service component
with use_ipopo(context) as ipopo:
ipopo.instantiate(
'pelix.http.service.async.factory', 'http-server',
{'pelix.http.address': 'localhost',
'pelix.http.port': 9000})
This code starts an HTTP server which will be listening on port 9000 and the HTTP service will be ready to handle requests. As no servlet service has been registered, the server will only return 404 errors.
API
Asynchronous HTTP service
The asynchronous HTTP service provides the same HTTP service interface as the synchronous service.
Synchronous Servlet service
This service is compatible with the synchronous servlets of the HTTP service. See HTTP Service for more information on the synchronous servlets.
Asynchronous Servlet service
To use the whiteboard pattern, an asynchronous servlet can be registered as a
service providing the pelix.http.servlet.async specification.
It must also have a valid pelix.http.path.async property, or it will be ignored.
Like for synchronous servlets, the binding methods described below have a
parameters argument, which represents a set of properties of the server,
given as a dictionary.
Some parameters can also be given when using the
register_servlet() method, with the parameters argument.
In any case, the following entries must be set by all implementations of the HTTP service and can’t be overridden when register a servlet. Note that their content and liability is implementation-dependent:
http.address: the binding address (str) of the HTTP server;http.port: the real listening port (int) of the HTTP server;http.https: a boolean flag indicating if the server is listening to HTTP (False) or HTTPS (True) requests;http.name: the name (str) of the server. If the server is an iPOPO component, it should be the instance name;http.extra: an implementation dependent set of properties.http.async: a boolean flag indicating if the servlet is asynchronous (True) or synchronous (False). This is set automatically by the HTTP service when registering the servlet.
An asynchronous servlet for the Pelix HTTP service has the following methods:
- class pelix.http.AsyncServlet
These are the methods that the HTTP service can call in a servlet. Note that it is not necessary to implement them all: the service has a default behaviour for missing methods.
- accept_binding(path: str, parameters: Dict[str, Any]) bool | None
This method is called before trying to bind the servlet. If it returns False, the servlet won’t be bound to the server. This allows a servlet service to be bound to a specific server.
If this method doesn’t exist or returns None or anything else but False, the calling HTTP service will consider that the servlet accepts to be bound to it.
- Parameters:
path (str) – The path of the servlet in the server
parameters (dict) – The parameters of the server
- bound_to(path: str, parameters: Dict[str, Any]) bool | None
This method is called when the servlet is bound to a path. If it returns False or raises an Exception, the registration is aborted.
- Parameters:
path (str) – The path of the servlet in the server
parameters (dict) – The parameters of the server
- unbound_from(path: str, parameters: Dict[str, Any]) None
This method is called when the servlet is bound to a path. The parameters are the ones given in
accept_binding()andbound_to().- Parameters:
path (str) – The path of the servlet in the server
parameters (dict) – The parameters of the server
- do_async_XXX(request: AbstractAsyncHTTPServletRequest, response: AbstractAsyncHTTPServletResponse) None
Each request is handled by the coroutine call
do_async_XXXwhereXXXis the name of an HTTP method (do_async_GET,do_async_POST,do_async_PUT,do_async_HEAD, …).If it raises an exception, the server automatically sends an HTTP 500 error page. In nominal behaviour, the method must use the
responseargument to send a reply to the client.- Parameters:
request – A
AbstractAsyncHTTPServletRequestrepresentation of the requestresponse – The
AbstractAsyncHTTPServletResponseobject to use to reply to the client
Asynchronous HTTP request
Each request method has a request helper argument, which implements the
AbstractAsyncHTTPServletRequest abstract class.
- class pelix.http.AbstractAsyncHTTPServletRequest
Asynchronous HTTP Servlet request helper
- abstractmethod get_client_address() Tuple[str, int]
Returns the address of the client
- Returns:
A (host, port) tuple
- abstractmethod get_command() str
Returns the HTTP verb (GET, POST, …) used for the request
- abstractmethod async get_header(name: str, default: Any = None) Any
Returns the value of a header
- Parameters:
name – Header name
default – Default value if the header doesn’t exist
- Returns:
The header value or the default one
- abstractmethod async get_headers() Dict[str, Any]
Returns a copy all headers, with a dictionary interface
- Returns:
A dictionary-like object
- abstractmethod get_path() str
Returns the request full path
- Returns:
A request full path (string)
- abstractmethod get_prefix_path() str
Returns the path to the servlet root
- Returns:
A request path (string)
- abstractmethod get_rfile() StreamReader
Returns the request input as a stream reader
- Returns:
A stream reader for the input stream
- abstractmethod get_sub_path() str
Returns the servlet-relative path, i.e. after the prefix
- Returns:
A request path (string)
- async read_data() bytes
Reads all the data in the input stream
- Returns:
The read data
Asynchronous HTTP response
Each request method also has a response helper argument, which implements the
AbstractAsyncHTTPServletResponse abstract class.
- class pelix.http.AbstractAsyncHTTPServletResponse
Asynchronous HTTP Servlet response helper
- abstractmethod async end_headers() None
Ends the headers part
- abstractmethod get_wfile() AbstractAsyncWriter
Retrieves the output as a writer.
end_headers()should have been called before.- Returns:
A writer for the output stream
- abstractmethod is_header_set(name: str) bool
Checks if the given header has already been set
- Parameters:
name – Header name
- Returns:
True if it has already been set
- async send_content(http_code: int, content: str, mime_type: str | None = 'text/html', http_message: str | None = None, content_length: int = -1) None
Utility method to send the given content as an answer. You can still use get_wfile or write afterwards, if you forced the content length.
If content_length is negative (default), it will be computed as the length of the content; if it is positive, the given value will be used; if it is None, the content-length header won’t be sent.
- Parameters:
http_code – HTTP result code
content – Data to be sent (must be a string)
mime_type – Content MIME type (content-type)
http_message – HTTP code description
content_length – Forced content length
- abstractmethod async send_sse(event: str | None = None, data: str | None = None, id: str | None = None) None
Sends a Server-Sent Event (SSE) message.
- Parameters:
event – The event name (optional)
data – The event data (optional)
id – The event ID (optional)
- abstractmethod set_header(name: str, value: Any) None
Sets the value of a header. This method should not be called after
end_headers().- Parameters:
name – Header name
value – Header value
- abstractmethod set_response(code: int, message: str | None = None) None
Sets the response line. This method should be the first called when sending an answer.
- Parameters:
code – HTTP result code
message – Associated message
- abstractmethod setup_sse(strict: bool = True) None
Sets up the response for Server-Sent Events (SSE)
- Parameters:
strict – If True, raises an error if the request is not for SSE
- abstractmethod async write(data: bytes) None
Writes the given data.
end_headers()should have been called before.- Parameters:
data – Data to be written
Write a servlet
This snippet shows how to write a component providing the servlet service:
from pelix.http import AsyncServlet, HTTP_SERVLET_ASYNC_PATH, \
AbstractAsyncHTTPServletRequest, AbstractAsyncHTTPServletResponse
from pelix.ipopo.decorators import ComponentFactory, Property, Provides, \
Requires, Validate, Invalidate, Unbind, Bind, Instantiate
@ComponentFactory(name='simple-async-servlet-factory')
@Instantiate('simple-servlet')
@Provides(AsyncServlet)
@Property('_path', HTTP_SERVLET_ASYNC_PATH, "/servlet")
class SimpleAsyncServletFactory:
"""
Simple servlet factory
"""
def __init__(self):
self._path = None
def bound_to(self, path, params):
"""
Servlet bound to a path
"""
print("Bound to", path)
return True
def unbound_from(self, path, params):
"""
Servlet unbound from a path
"""
print("Unbound from", path)
return None
async def do_async_GET(self,
request: AbstractAsyncHTTPServletRequest,
response: AbstractAsyncHTTPServletResponse):
"""
Handle a GET
"""
await response.send_content(200, "Hello, Async World!", mime_type="text/plain")
To test this snippet, install and start this bundle and the HTTP service bundle in a framework, then open a browser to the servlet URL. If you used the HTTP service instantiation sample, this URL should be http://localhost:9000/servlet.
Server-Sent Events (SSE) Endpoint
Server-Sent Events (SSE) allow servers to push updates to clients over HTTP and are supported by the asynchronous HTTP service.
An SSE endpoint can be implemented by an asynchronous servlet in a
do_async_XXX method (usually, do_async_GET):
Call the
setup_sse()method to set up the response for SSE. By default, this method will fail if the client doesn’t explicitly request SSE by setting theAccept: text/event-streamheader. You can set thestrictparameter toFalseto bypass this check.Call the
end_headers()method to finalize the headers.Use the
send_sse()method to send events to the client.
Here is an example of an SSE endpoint:
from pelix.http import AsyncServlet, HTTP_SERVLET_ASYNC_PATH, \
AbstractAsyncHTTPServletRequest, AbstractAsyncHTTPServletResponse
from pelix.ipopo.decorators import ComponentFactory, Provides, Property
@ComponentFactory()
@Provides(AsyncServlet)
@Property("_path", HTTP_SERVLET_ASYNC_PATH, "/sse")
class SSEServlet(AsyncServlet):
async def do_async_GET(self, request: AbstractAsyncHTTPServletRequest, response: AbstractAsyncHTTPServletResponse):
response.setup_sse()
await response.end_headers()
for i in range(5):
await response.send_sse(f"Message {i}", event="update")
await asyncio.sleep(1)
WebSocket Server Endpoints
Pelix also supports WebSocket server endpoints through the pelix.http.websocket.handler specification.
To create a WebSocket handler, inherit from the AbstractWebSocketHandler abstract class and implement the required methods.
- class pelix.http.AbstractWebSocketHandler(*args, **kwargs)
Abstract class for WebSocket handlers
This class can be used to implement a WebSocket handler with default methods.
- async ws_accept(request: AbstractAsyncHTTPServletRequest) bool
Accepts all WebSocket connections by default
- async ws_binary(session: WebSocketSession, message: bytes) None
Default implementation does nothing
- async ws_close(session: WebSocketSession, code: int, reason: str) None
Default implementation does nothing
- async ws_error(session: WebSocketSession, error: str) None
Default implementation does nothing
- async ws_message(session: WebSocketSession, message: str) None
Default implementation does nothing
- async ws_open(session: WebSocketSession, request: AbstractAsyncHTTPServletRequest) None
Default implementation does nothing
- class pelix.http.WebSocketSession(*args, **kwargs)
Websocket session helper
- async close(code: int = 1000, reason: str | None = None) None
Closes the WebSocket session
- Parameters:
code – Close code (default is 1000, normal closure)
reason – Optional reason for the closure
- get_client_address() Tuple[str, int]
Returns the address of the client
- Returns:
A (host, port) tuple
- async send_binary(message: bytes) None
Sends a binary message to the client
- Parameters:
message – Binary message to send
- async send_text(message: str) None
Sends a message to the client
- Parameters:
message – Message to send
Here is an example:
from pelix.http import AbstractWebSocketHandler, HTTP_WEBSOCKET_PATH, \
AbstractAsyncHTTPServletRequest, WebSocketSession
from pelix.ipopo.decorators import ComponentFactory, Provides, Property
@ComponentFactory()
@Provides(AbstractWebSocketHandler)
@Property("_path", HTTP_WEBSOCKET_PATH, "/ws")
class SampleWebSocketHandler(AbstractWebSocketHandler):
async def ws_open(self, session: WebSocketSession, request: AbstractAsyncHTTPServletRequest):
print("WebSocket connection opened")
async def ws_message(self, session: WebSocketSession, message: str):
print("Received message:", message)
await session.send_text(f"Echo: {message}")
async def ws_close(self, session: WebSocketSession, code: int, reason: str):
print("WebSocket connection closed")