benzene.http

The inbound HTTP (ASGI) transport binding: host the handlers you wrote against benzene.core behind a real HTTP server. Distribution: benzene-http (depends on benzene-core).

pip install benzene-http

Overview

BenzeneHttpApp is a standard ASGI application implementing the HTTP binding from the specification (transport-bindings §2):

Routing

Pair @http_endpoint(method, path) with @message(topic). The HTTP decorator says where a request arrives; @message says which handler it resolves to. Stack @http_endpoint to give one handler several routes.

from benzene.core import message
from benzene.results import Result
from benzene.http import BenzeneHttpApp, HttpRouter, http_endpoint

@http_endpoint("GET", "/orders/{id}")
@message("order:get")
async def get_order(request: dict) -> Result:
    return Result.ok({"id": request["id"]})

app = BenzeneHttpApp(HttpRouter().add(get_order))   # run: uvicorn module:app

How the request is assembled

The handler's request is the JSON body object merged with the query string and then the captured path parameters — path wins, then query, then body. So /orders/{id} delivers id as a request field even if the body also has one.

Versioned routes

A {version} path segment is treated specially (versioning.md §2): it drives handler selection rather than becoming a request field. Register one route with the segment and the versioned handlers in the message registry, and /v1/orders and /v2/orders reach the v1 and v2 handlers:

router.register("GET", "/{version}/orders/{id}", "order:get", get_order)

The route segment is sugar for the benzene-version header (versioning.md §2.1): it is supplied exactly as if the caller had sent the header, and is authoritative over both the route's static version and a header the caller did send. Without a {version} segment, a caller's benzene-version header overrides the route's static version; an unrelated version/x-version header does not.

version is a reserved path-parameter name: a {version} segment is consumed for handler selection and is never delivered to the handler as a request field, so don't use {version} for a genuine domain field (name it e.g. {revision} instead). See versioning in benzene.core.

Status mapping

to_http(status) and from_http(code) implement the wire-contracts §4.1 table:

from benzene.http import to_http, from_http

to_http("ok")          # 200
to_http("created")     # 201
to_http("not-found")   # 404
from_http(422)         # "validation-error"

BenzeneHttpApp

BenzeneHttpApp(router, application=None, pipeline=None, container=None, *, standard_paths=None)

By default it builds a BenzeneMessageApplication from the router's handler definitions. Pass your own application (or a pipeline / container) to add middleware or DI registrations.

Two ways to invoke it:

Well-known surfaces (the Cloud Service Profile)

Pass standard_paths=StandardPaths(...) to expose the profile's well-known operational surfaces under a configurable /benzene/ prefix (transport-bindings §1; profile R3/R4/R5/R7). They are served ahead of ordinary routing and never shadow your routes.

from benzene.core import AppDefinition, ContractInfo
from benzene.http import BenzeneHttpApp, StandardPaths

app = BenzeneHttpApp.from_definition(
    AppDefinition(
        router=router,
        standard_paths=StandardPaths(
            health=health_checks,           # enables GET /benzene/health
            # invoke and spec are on by default -> POST /benzene/invoke, GET /benzene/spec
        ),
        contract=ContractInfo("orders"),    # the self-description GET /benzene/spec answers with
    )
)

?type= and ?format= on /benzene/spec

The route copies its type and format query parameters onto the benzene:spec request (contract-document §7). Absent, empty, type=benzene and format=json (case-insensitive) mean the Contract Document; any other type (openapi, asyncapi, …) or format is the topic's not-implemented, mapped to 501 — never a different document under a name that does not describe it. (For an OpenAPI document, use benzene-openapi and serve it where you like.)

The document carries this host's own knowledge — messageEndpoint (the envelope endpoint, absent when invoke=False) and each topic's httpMappings from the router — alongside the ContractInfo's transports, because application_from derives it from the whole AppDefinition.

The three cloud hosts drive their HTTP trigger through this same BenzeneHttpApp, so passing standard_paths= to GcpFunctionsApp / AwsLambdaApp / AzureFunctionsApp exposes the identical surfaces on a Lambda, Cloud Function, or Azure Function.

Outbound — HttpMessageSender

The reverse direction: a MessageSender that publishes a message to another Benzene service over HTTP POST and maps the response back (transport-bindings §2): a failure's problem document supplies the status (its benzeneStatus, so an application-defined status survives the 500 it travelled as) and the errors; from_http supplies the status only for a body that states none. It forwards the Benzene headers as HTTP headers (plus the reserved topic), so correlation ids and trace context propagate.

from benzene.http import HttpMessageSender

sender = HttpMessageSender("https://orders.svc")          # topic -> {base}/{topic}
result = await sender.send_message("orders:place", {"sku": "A"}, headers={"x-correlation-id": "c1"})
# a 201 -> result.status == "created"; the response body becomes result.payload

Serving it alongside another transport

BenzeneHttpApp is a plain ASGI app, so the ordinary way to run it is uvicorn my_service:app — that stays the right answer for an HTTP-only service. For a process that serves HTTP and polls a queue, the server has to run as one leg of a benzene.core.WorkerHost:

from benzene.http import asgi_server_worker, uvicorn_worker

WorkerHost().add("http", uvicorn_worker(app, port=8080, access_log=False))

# one level down: build the server yourself, then adapt it
server = uvicorn.Server(uvicorn.Config(app, host="0.0.0.0", port=8080))
WorkerHost().add("http", asgi_server_worker(server))

Exports

BenzeneHttpApp, HttpResponse, HttpRouter, HttpEndpoint, http_endpoint, routes_of, to_http, from_http, HttpMessageSender, HttpReply, HttpTransport, stdlib_transport, StandardPaths, DEFAULT_PREFIX, uvicorn_worker, asgi_server_worker, SupportsAsgiServing.

See also