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() and bound_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_XXX where XXX is 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 response argument to send a reply to the client.

Parameters:

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):

  1. 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 the Accept: text/event-stream header. You can set the strict parameter to False to bypass this check.

  2. Call the end_headers() method to finalize the headers.

  3. 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")