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 authoritative over both the route's static version and a caller's version header. Without a {version} segment, a caller's version header (any of the fallback names — benzene-version, version, x-version) overrides the route's static version.

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 (design-principles §5.2; profile R3/R4/R5/R7). They are served ahead of ordinary routing and never shadow your routes.

from benzene.core import HealthChecks, ServiceSpec
from benzene.http import BenzeneHttpApp, StandardPaths

app = BenzeneHttpApp(
    router,
    application=application,
    standard_paths=StandardPaths(
        health=health_checks,                              # enables GET /benzene/health
        spec=ServiceSpec.derive(registry, service="orders"),  # enables GET /benzene/spec
        # invoke is on by default -> POST /benzene/invoke
    ),
)

The reserved topic benzene:spec is answered on any transport by spec_interception (the same pattern as health and mesh interception); the HTTP /benzene/spec surface is its HTTP face.

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 via from_http (transport-bindings §2). 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

Exports

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

See also