Verified linked Cargo versioned Markdown · en

usage

lenso.web-ingress@0.4.9 · document web-component-guide@954a00b89b095f20fafb68071b7252d5d2c2e67b

Find documentation for 0.4.9

Search only build-verified Markdown for this exact Plugin version.

Enter a term to search this release's documentation.

Lenso Web

General-purpose backend Web Interfaces and Plugins for Lenso.

This repository owns:

  • lenso.http.endpoint@1, provided by backend Plugins that own HTTP behavior;
  • lenso.http.stream-endpoint@1, provided when a response must remain incremental;
  • lenso-web-ingress-plugin, which assembles immutable routes and owns HTTP transport;
  • lenso.http.client@1, required by backend Plugins that call upstream HTTP APIs;
  • lenso-http-egress-plugin, which provides policy-bounded outbound HTTP transport;
  • credential-evidence extraction before a target-owned Auth decision; and
  • protocol response mapping after the target Plugin completes.

It does not own portable Kernel semantics, authentication policy, business authorization, Console/UI behavior, Web Shell, Browser Adapter, or a global route registry.

This document makes no release, target, or production-maturity claim. Those facts require their exact CI, registry, or deployment receipts.

Current packages

  • lenso-capability-http-endpoint
  • lenso-capability-http-stream-endpoint
  • lenso-capability-http-client
  • lenso-http-auth
  • lenso-http-egress-plugin
  • lenso-openapi-plugin
  • lenso-web-ingress-plugin
  • lenso-web-host

lenso-http-egress-plugin and lenso-openapi-plugin use the ordinary source-first native authoring path: struct Plugins declare configuration, typed Capability Ports, provided Capabilities, and lifecycle hooks; linked factory registration and Provider endpoints are generated. App Composition still decides whether an Instance exists and exactly which bindings it receives.

lenso-web-ingress-plugin remains the deliberate compatibility exception. It is an endpoint-free consumer of many lenso.http.endpoint@1 and many lenso.http.stream-endpoint@1; its public Factory also accepts Host-owned middleware, listener observation, and lane-replica handles. The current struct authoring macro cannot inject those Host-only values into a consumer-only Plugin. Ingress routing nevertheless uses the generated typed ManyPort<EndpointClient>; raw request handles are confined to generated Capability projection code and test fixtures.

The Capability crates keep their generated Rust bindings. Bun consumers import the matching TypeScript projections from @lenso/bun, which locks the source revision and verifies each projection independently.

WebIngressConfig is immutable Plugin Instance configuration from the Resolved App Plan and defaults to an ephemeral loopback listener. App Composition may explicitly bind a fixed private or public address; deployment policy stays with the host. Ingress serves HTTP/1.1 and cleartext HTTP/2 prior knowledge, accepts one Authorization credential, replaces untrusted external request identifiers, removes credential and hop-by-hop headers before dispatch, enforces body/head, concurrency, and Endpoint deadline limits, and cooperatively cancels work on client disconnect or App shutdown. Request bodies are collected frame-by-frame under the configured limit and avoid a copy when Hyper supplies one data frame.

Customized configuration uses crates/lenso-web-ingress-plugin/config.schema.json. All fields are optional:

{
  "bind_address": "0.0.0.0:8080",
  "max_request_body_bytes": 1048576,
  "max_request_head_bytes": 16384,
  "max_concurrent_requests": 128,
  "max_connections": 1024,
  "request_head_timeout_millis": 10000,
  "request_body_timeout_millis": 30000,
  "connection_idle_timeout_millis": 60000,
  "shutdown_grace_timeout_millis": 30000,
  "request_timeout_millis": 30000,
  "session_cookie": {
    "name": "__Host-lenso-session",
    "csrf_cookie_name": "__Host-lenso-csrf",
    "csrf_header_name": "x-csrf-token"
  }
}

session_cookie is optional. Both configured Cookie names must use the __Host- prefix. Ingress maps the exact session Cookie to protocol-neutral credential scheme session. Unsafe Cookie-authenticated methods require the configured CSRF Cookie and request Header to match; missing or mismatched evidence is rejected before Endpoint dispatch. Authorization continues to work when no session Cookie is selected. Supplying both credentials is ambiguous and fails closed. Cookie and CSRF protocol headers are never forwarded to the Endpoint Plugin.

An existing composition with empty Ingress configuration continues to receive these defaults without a schema. Only customized Plan configuration needs the schema.

max_connections is a hard listener-group budget. The idle deadline is suspended while any parsed request is active and starts at the final request's completion, so it limits keep-alive connections without imposing an absolute connection lifetime. request_body_timeout_millis is a total body-read deadline. request_head_timeout_millis configures Hyper's HTTP/1 header deadline; an HTTP/2 connection with no active parsed stream is still bounded by the connection idle deadline. HTTP/2 advertises max_concurrent_requests as its per-connection stream ceiling; a peer that opens excess simultaneous streams receives REFUSED_STREAM and may retry, while the same semaphore remains the global execution ceiling across connections. On App shutdown, admitted connections receive shutdown_grace_timeout_millis to drain after Hyper begins graceful shutdown; the default matches the Endpoint request deadline, after which the socket and its live-connection permit are released.

Ingress admission and Kernel Endpoint admission are separate resolved-Plan boundaries. A Host should explicitly apply the Ingress execution ceiling to every lenso.http.endpoint@1 binding it creates; the helper returns the copyable queue/concurrency pair without moving policy into the Capability Descriptor:

let (queue_capacity, max_concurrency) = ingress_config.endpoint_admission_limits();
let binding = HostBinding::new(
    PluginInstanceId::new("lenso.web-ingress", "default"),
    CAPABILITY_ID,
    "http-endpoints",
)
.with_admission(RequestAdmissionPlan::new(queue_capacity, max_concurrency));

A Host may deliberately choose a stricter Endpoint binding. Omitting the override inherits the generic source-first provider default of one execution slot and no queue, which is usually inappropriate for an HTTP provider.

Route providers are resolved through immutable many bindings. They describe their method/path table during activation, before the App Ready Gate opens; route collisions fail startup instead of changing behavior at runtime.

Streaming providers use distinct describe_stream and handle_stream operations so one cohesive Plugin can provide both HTTP capabilities without Rust method-name collisions. handle_stream first emits exactly one head frame containing status and headers, then zero or more chunk frames containing body bytes, followed by a terminal event. Ingress never buffers those chunks. Malformed ordering or transport-owned response headers fail closed. Global Ingress middleware can inspect and modify the response head; it does not buffer or rewrite the stream body.

Endpoint providers can put route attributes directly on handlers. The outer #[endpoint] attribute collects them at compile time and generates both the Capability description and handler dispatch:

#[lenso::plugin]
#[derive(Clone, Debug)]
struct OrdersHttp {}

#[derive(serde::Deserialize)]
struct CreateOrder {
    total_cents: u64,
}

#[derive(serde::Serialize)]
struct CreatedOrder<'a> {
    id: &'a str,
}

#[endpoint]
impl OrdersHttp {
    #[post("orders.create", "/orders")]
    async fn create(
        &self,
        Json(_order): Json<CreateOrder>,
    ) -> Result<(StatusCode, Json<CreatedOrder<'static>>), Problem> {
        Ok((StatusCode::CREATED, Json(CreatedOrder { id: "order-42" })))
    }
}

#[lenso::plugin] declares the Plugin boundary. #[endpoint] publishes its HTTP Endpoint Capability and generates route description and dispatch from the same handler declarations. Json<T> and (StatusCode, T) implement IntoResponse; Problem is the typed intentional-error response. Lower-level response and header helpers remain available for unusual responses.

The supported method attributes are get, post, put, patch, delete, head, options, and query. Each handler declares its stable route ID and path. http_endpoint! remains available for existing providers and generated source that prefers one explicit route table.

Attribute-authored handlers may use Path<T>, QueryParams<T>, Json<T>, Option<Json<T>>, Body, Headers, and RequestId extractors. Body preserves raw bytes, while Headers::get performs case-insensitive lookup. Middleware on the impl applies to every route; route-owned middleware follows it, before extraction:

#[endpoint]
#[middleware(trace_all)]
impl OrdersHttp {
    #[middleware(trace_request)]
    #[get("orders.read", "/orders/{order_id}")]
    async fn read(
        &self,
        context: InvocationContext,
        Path(path): Path<OrderPath>,
    ) -> Result<HandleResponse, EndpointHandleInvocationError> {
        self.read_authenticated(context, path.order_id).await
    }
}

The named async middleware method receives InvocationContext and HandleRequest, then returns MiddlewareOutcome::next with an enriched context/request or MiddlewareOutcome::response to short-circuit. Multiple provider-wide and route middleware declarations run in order, with provider-wide middleware first. Custom protocol-local extractors can implement FromRequest<Provider>; extractors are asynchronous, can access the provider's activation-time clients, and can enrich the context seen by later extractors and the handler.

The macro rejects empty or malformed routes, duplicate route identifiers, and duplicate exact method/path pairs during compilation. Web Ingress still owns method/path matching, path-parameter extraction, semantic path-shape and cross-provider collision detection, and transport limits. Endpoint handlers own authentication orchestration, request decoding, business Capability calls, and intentional HTTP responses.

A linked native Host that only needs Ingress plus the Endpoint Plugins in the binary can skip a handwritten Plan:

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), lenso_web_host::WebHostError> {
    lenso_web_host::NativeWebHost::new()
        .plugin::<GreetingsHttp>()
        .bind("127.0.0.1:8080".parse().unwrap())
        .run()
        .await
}
cargo run -p lenso-web-greetings-app-example

NativeWebHost is a Web Host preset. Inventory admits linked Factories. .plugin::<T>() selects a default Instance; .plugin_with / .instance pass configuration; .plugin_on_lane, .plugin_with_on_lane, and .instance_on_lane record explicit Execution Lane placement; .resolve_plan() exposes the exact immutable Plan for an advanced Runner integration; .factory installs a hand-written native Factory for the single-lane path; .with_ingress_config supplies limits, deadlines, cookies, and WebSocket policy; .with_middleware adds one Ingress middleware chain; .with_diagnostics adds one Host-owned observer for Endpoint failures; .with_tower_middleware / .with_tower_layer adapt a Tower pre-dispatch policy; .with_adapter adds Bun/WASI/process Adapters. .with_replicated_ingress creates lane-local Ingress customization; .with_replicated_lane creates the complete lane-local native registry and Adapter catalog; .with_replicated_ready_timeout sets the replicated Ready deadline; .start_replicated binds one shared listener and starts one Kernel per declared lane; .run_replicated adds the private LocalSet and Ctrl-C lifecycle. Ingress is a Host default. Enabled HTTP, stream, and WebSocket Endpoint providers are bound to it. The middleware chain and diagnostics observer are shared by native and start_event Hosts. Middleware runs after transport normalization and unwinds response hooks in reverse order. A diagnostics observer sees only trusted request/route/provider identity and the internal Runtime Failure; it cannot change routing or the HTTP response. A Tower policy receives a cloned normalized request and returns Continue or Respond; it does not replace Ingress's streaming/WebSocket service. Linking a crate without .plugin does not start it. Unique Capability requirements still derive. Call start from an existing LocalSet when a test needs the bound address. For route-contract tests, start_event exposes the same ingress behavior without opening a TCP socket:

let app = NativeWebHost::new().plugin::<GreetingsHttp>().start_event().await?;
let response = app.handle(Request::get("/missing").body(Bytes::new())?).await?;
assert_eq!(response.status(), 404);
app.shutdown().await?;

handle_response is the streaming/WebSocket-capable variant when a test or embedded Host must preserve the response body kind instead of buffering it. Both running Host handles expose route_manifest() after activation, so route contract tests can assert the exact immutable dispatch surface.

The Host deliberately inherits Lenso's portable local execution lane: native Plugin state and Endpoint futures may be !Send/!Sync, so start runs on a Tokio current_thread runtime and LocalSet. start_replicated preserves that boundary by letting ReplicatedNativeApp own one current-thread Kernel per Plan-declared lane. It builds the Ingress factory and inventory-linked native registry inside each lane, then uses WebIngressListenerCoordinator for one shared listener. Lane-local custom middleware, hand-written Factories, and non-native Adapters must be created through the with_replicated_* builders; ordinary single-lane Rc values are rejected rather than shared unsafely.

For authenticated HTTP, Ingress selects one Authorization credential into HandleRequest::credential; it does not decide identity or permission. The lenso-http-auth integration crate handles Auth invocation, stable 401/403 responses, and assertion attachment while the application owns meaningful actor types:

struct UserActor { subject: String }

impl AuthenticatedHttpActor for UserActor {
    const KIND: &'static str = "user";

    fn from_assertion(assertion: &ActorAssertion) -> Self {
        Self { subject: assertion.subject().to_owned() }
    }
}

impl FromRequest<OrdersHttp> for UserActor {
    fn from_request<'a>(
        provider: &'a OrdersHttp,
        context: &'a mut InvocationContext,
        request: &'a HandleRequest,
    ) -> ExtractorFuture<'a, Self> {
        extract_authenticated_actor(provider, context, request)
    }
}

The Endpoint provider implements AuthClientSource to return its explicitly bound activation-time AuthClient. A handler can then request UserActor directly; an endpoint for a distinct credential actor kind can define AdminActor in the same way. These AuthenticatedHttpActor types are edge authentication projections, not permission shortcuts: roles, tenant access, resource ownership, and every final authorization decision remain in the target business Plugin, which verifies the attached assertion. Release clients during deactivation rather than resolving bindings per request.

lenso-http-auth is optional integration glue. Neither portable HTTP Capability crate nor the Web Ingress library has a normal dependency on it; removing the helper and its application integration leaves unauthenticated or otherwise authenticated Endpoint providers usable without a Kernel branch.

The SDK is additive: existing providers that implement EndpointProvider directly continue to work through the same immutable activation and dispatch path.

Optional OpenAPI 3.1 documents

OpenAPI is an opt-in Plugin, not an Ingress mode. Linking lenso-openapi-plugin does not add a route or change an App. App Composition enables it by selecting one lenso.openapi Instance, binding the Endpoint providers to document to that Instance, and binding the Instance's own HTTP Endpoint to Web Ingress. Removing that package selection, Instance, and those bindings removes the document without changing the business Endpoints.

For a generated local Rust Web App, lenso app add @lenso/openapi --root . creates a pinned shared source link and an explicit plugins/lenso.openapi/default.toml selection. Its generated Host binds the document Plugin's many Endpoint requirement to the web Slot; the document Endpoint itself and Endpoint providers in other Slots are not document inputs. After lenso app build, lenso app check --root dist, and lenso app start --from dist, fetch the actual /openapi.json from that App for lenso-web-client generate openapi.json src/generated/lenso-api.ts. lenso plugins disable lenso.openapi default --root dist changes the built App's Plugin Root; lenso app start --from dist --root dist uses that external Root instead of the immutable build snapshot and no longer serves the document route. This is a native local-Host path; it does not imply Workers or Wasm Endpoint qualification. The generated HTML home route remains an undocumented-response fallback in this document; client generation does not provide a typed HTML response for it.

Endpoint authors may attach an OpenAPI 3.1 Operation Object while retaining the stable route declaration as the source of operationId, method, and path:

#[endpoint]
impl OrdersHttp {
    #[get("orders.read", "/orders/{order_id}")]
    #[openapi({
        summary: "Read an order",
        responses: {
            "200": {
                description: "Order",
                content: {
                    "application/json": {
                        schema: {
                            "type": "object",
                            required: ["id"],
                            properties: { id: { "type": "string" } }
                        }
                    }
                }
            }
        }
    })]
    async fn read(&self) -> Result<HandleResponse, EndpointHandleInvocationError> {
        // ...
    }
}

The JSON-like syntax accepts ordinary identifier keys without quotes. Keys such as "type", "application/json", and "$ref" stay quoted. String, number, boolean, null, array, and nested object values are supported. The previous JSON string form remains compatible.

http_endpoint! accepts the same syntax through openapi = openapi_operation!({ ... }). The authoring macros validate the DSL at compile time and reject a separately declared operationId. A route without metadata remains valid and receives an Undocumented response. fallback only when an OpenAPI document is assembled.

Strict public API contracts

An Endpoint that is deliberately published for generated clients can opt into a strict check without changing App Composition or creating a second request/ response DTO. Add #[openapi_contract] beside the existing #[openapi(...)] metadata. The typed values already used by the handler remain the source of the checked representation:

use lenso_capability_http_endpoint::{Json, JsonSchema, Path, endpoint};
use lenso_capability_http_endpoint::response::{Problem, StatusCode};

#[derive(serde::Deserialize, JsonSchema)]
struct CreateOrder { id: String }

#[derive(serde::Serialize, JsonSchema)]
struct CreatedOrder { id: String }

#[endpoint]
impl OrdersHttp {
    #[post("orders.create", "/orders/{account_id}")]
    #[openapi({ /* the complete authored OpenAPI Operation Object */ })]
    #[openapi_contract(
        success = 201,
        errors = [(422, "invalid_order")]
    )]
    async fn create(
        &self,
        Path(path): Path<OrderPath>,
        Json(input): Json<CreateOrder>,
    ) -> Result<(StatusCode, Json<CreatedOrder>), Problem> {
        # todo!()
    }
}

The Plugin package must make the schemars derive available (for example, schemars = "1.2"), then derive JsonSchema for each public Path, QueryParams, Json request body, and JSON response value. At OpenAPI Plugin activation, the existing generated Endpoint description carries a temporary type-derived fragment. The document assembler compares it with the author's Operation Object and rejects drift in the route ID, method, path, path/query parameters, JSON request body, JSON success response, and known RFC 9457 Problem responses. The fragment is removed before /openapi.json is served. The comparison also rejects non-default path/query serialization settings, so the emitted operation remains compatible with an ordinary OpenAPI-generated client rather than only looking superficially similar.

Json<T> has an implicit 200 success response. For (StatusCode, Json<T>), declare the exact success = 2xx status. A Problem handler error must enumerate its stable error codes through errors = [(4xx_or_5xx, "code")]; JSON, path, and query extractor failures are included automatically. Strict contracts intentionally support the typed extractors Path<T>, QueryParams<T>, Json<T>, and Option<Json<T>>, plus RequestId and InvocationContext. Raw bodies, headers, requests, and custom extractors stay outside this first representation-level check rather than being silently documented incorrectly.

#[openapi_contract] is opt-in. Existing Endpoint providers, private routes, and routes with ordinary #[openapi] metadata continue to assemble exactly as before; they are not forced to supply complete public-client contracts.

The selected lenso.openapi Instance uses immutable configuration validated by crates/lenso-openapi-plugin/config.schema.json:

{
  "title": "Orders API",
  "version": "1.0.0",
  "description": "Public order operations",
  "document_path": "/openapi.json",
  "servers": [{"url": "https://api.example.com"}],
  "components": {
    "securitySchemes": {
      "bearer": {"type": "http", "scheme": "bearer"}
    }
  }
}

The Plugin never infers a public server address from its listener and never discovers Endpoint providers globally. Only providers explicitly bound to its many lenso.http.endpoint@1 requirement appear in the document. Swagger UI, Redoc, pages, assets, and navigation remain with the application or Console UI owner rather than this repository.

Ingress-owned failures have stable JSON codes. Missing routes return 404, method mismatches return 405 with Allow, malformed evidence returns 400, body/head limits return 413/431, invalid or rejected Endpoint responses return 502, unavailable Endpoint execution returns 503, and Endpoint deadlines return 504. Every Ingress-produced response carries a generated x-request-id and x-content-type-options: nosniff.

Hosts may install WebIngressDiagnostics on the concrete Ingress factory to observe an Endpoint Runtime Failure together with its trusted request ID, route ID, and bound provider index before the failure is mapped to a generic 503 or 504. Diagnostics receive neither credentials nor request bodies and cannot change routing or the HTTP response:

#[derive(Debug)]
struct DevelopmentDiagnostics;

impl WebIngressDiagnostics for DevelopmentDiagnostics {
    fn endpoint_runtime_failure(&self, event: WebIngressEndpointFailure<'_>) {
        tracing::warn!(
            request_id = event.request_id(),
            route_id = event.route_id(),
            provider_index = event.provider_index(),
            failure = ?event.failure(),
            "HTTP Endpoint failed",
        );
    }
}

let ingress = WebIngressFactory::new().with_diagnostics(DevelopmentDiagnostics);

Hosts can install transport-wide policy on one concrete Ingress factory. Global Ingress middleware runs after request-head/body limits, request-ID replacement, hop-by-hop filtering, and credential isolation. Request steps run in declaration order before route matching; response steps run in reverse order, including for short-circuited responses:

use futures::future::LocalBoxFuture;
use lenso_kernel::RuntimeFailure;
use lenso_web_ingress_plugin::{
    WebIngressFactory, WebIngressMiddleware, WebIngressMiddlewareOutcome,
    WebIngressRequest, WebIngressResponse,
};

#[derive(Debug)]
struct AccessLog;

impl WebIngressMiddleware for AccessLog {
    fn identity(&self) -> &'static str {
        // Include immutable settings so replicated lanes can compare policy.
        "access-log:json:v1"
    }

    fn before_request<'a>(
        &'a self,
        request: &'a mut WebIngressRequest,
    ) -> LocalBoxFuture<'a, Result<WebIngressMiddlewareOutcome, RuntimeFailure>> {
        tracing::info!(method = %request.method(), path = %request.uri().path());
        Box::pin(async { Ok(WebIngressMiddlewareOutcome::Continue) })
    }

    fn after_response<'a>(
        &'a self,
        _request: &'a WebIngressRequest,
        response: &'a mut WebIngressResponse,
    ) -> LocalBoxFuture<'a, Result<(), RuntimeFailure>> {
        tracing::info!(status = response.status().as_u16());
        Box::pin(async { Ok(()) })
    }
}

let ingress = WebIngressFactory::new().with_middleware(AccessLog);

This seam is suitable for transport-wide access logging, CORS, compression, and coarse network admission policy. Middleware can mutate the normalized method, URI, headers, and body, or return an intentional response. It cannot override Ingress-owned response headers, reintroduce credential or hop-by-hop headers to an Endpoint, bypass immutable transfer limits, or access Hyper's connection body. Middleware failures map to 503.

Authentication remains Endpoint orchestration: use UserActor or AdminActor extractors backed by the Auth Plugin so the target Plugin can still make the final authorization decision. Do not turn global Ingress middleware into a business identity or permission registry.

Hosts that replicate one App across Runner lanes can bind once and create one Ingress factory per lane through WebIngressListenerCoordinator. The coordinator opens the Ready Gate only after every replica publishes the same canonical route manifest and ordered middleware identity sequence, distributes accepted sockets round-robin, keeps connection and request budgets global to the listener group, and gives each lane a fair local request bound as a second containment boundary. Acceptor failures are propagated to every lane instead of silently stopping the listener. Every identity must include its immutable configuration:

let coordinator = WebIngressListenerCoordinator::bind(config, lane_count).await?;
let factories = (0..lane_count)
    .map(|lane_index| {
        WebIngressFactory::replicated_at(&coordinator, lane_index).map(|factory| {
            factory.with_middleware(AccessLog)
        })
    })
    .collect::<Result<Vec<_>, _>>()?;

replicated_at is preferred when a Runner builds lane-local factories: the explicit slot keeps a lane mapped to the same replica even when lane threads start in a different order. replicated remains available for sequential factory construction and allocates the next free slot.

The coordinator is only the transport fan-out. NativeWebHost::start_replicated() now packages the native Runner composition around it: it resolves one Plan, creates one deterministic replica slot per declared lane, builds each lane-local NativePluginRegistry from inventory, and starts lenso_runner::ReplicatedNativeApp with one ExecutionAdapterCatalog per lane. Use the ordinary NativeWebHost::start() for a single Kernel; never call it once per lane, because that would create independent listeners and bypass the Runner's cross-lane Plan validation.

For a lane-local custom Adapter or hand-written Factory, extend the complete catalog supplied to with_replicated_lane rather than constructing a second Runner. The callback runs in the target lane, so it may safely create !Send/!Sync Plugin state there:

let app = NativeWebHost::new()
    .plugin::<GreetingsHttp>()
    .plugin_on_lane::<OrdersHttp>("orders")
    .with_replicated_lane(|_lane, registry| {
        let mut catalog = ExecutionAdapterCatalog::single(registry);
        catalog = catalog
            .with_adapter(MyLaneLocalAdapter)
            .map_err(|error| error.to_string())?;
        Ok(catalog)
    })
    .start_replicated()
    .await?;

The default lane builder already supplies the native catalog, so this callback is only needed for additional lane-local implementations. The single-lane .factory(...) installer is intentionally rejected by start_replicated; add the hand-written factory inside this callback once per lane instead.

HttpEgressConfig requires at least one exact http or https origin. The binding and immutable origin list are the caller's outbound authority. Egress rejects origin changes, user info, URL fragments, authority/hop-by-hop request headers, and oversized transfer evidence. It does not follow redirects, use system proxies, set a Referer, store cookies, or retry automatically. Total and connect timeouts plus concurrency and head/body limits are owned by each Egress Instance. Egress negotiates HTTP/2 over TLS by default while retaining HTTP/1.1 compatibility. An Instance may instead require HTTP/1.1 or HTTP/2 prior knowledge, including cleartext h2c for trusted internal origins:

{
  "allowed_origins": ["http://inventory.internal:8080"],
  "http_version": "http2_prior_knowledge"
}

HTTP/3 is not currently shipped: Reqwest 0.13 exposes its HTTP/3 client only behind the upstream reqwest_unstable configuration, while Ingress still needs a Host-owned TLS credential source and QUIC/UDP listener lifecycle.

Verify

cargo fmt --all -- --check
cargo check --locked --workspace --all-targets
cargo test --locked --workspace
cargo clippy --locked --workspace --all-targets -- -D warnings

For a bounded portable HTTP Endpoint proof, run cargo test --locked -p lenso-web-ingress-plugin --test portable_http_component. The fixture compiles the same business handler into a native provider and a real Wasm Component, then sends six loopback HTTP requests through Web Ingress to each: method/path matching, non-UTF-8 request and response bytes, 404/405, domain rejection, and runtime failure. This is local Native Host plus Wasmtime evidence. It does not qualify workerd or deployed Workers, outbound HTTP, streaming, WebSocket, authentication, or production resource ceilings. The Component above runs through the Native Wasmtime Adapter. The workspace also has a bounded lenso app build --target workers path for one verified V4/V6 Workers Component variant. It packages a locked Plan, Jco bindings, the pinned @lenso/workers-runtime HTTP event adapter, and a workers-build.json build receipt. See crates/lenso-engine-app/assets/workers-app-README.md for the request-only local-workerd limits. A build receipt or Node handler test is not a real workerd run: the exact output needs its own HTTP corpus. Neither this Native test nor a local workerd pass qualifies deployed Workers, streams, WebSockets, outbound HTTP, cancellation, or production resource ceilings.

The dispatch-only release workflow publishes an explicitly authorized package set from a landed main SHA through configured crates.io Trusted Publishers and GitHub OIDC. Dependency-aware publication releases the Capability crates before the dependent lenso-web-ingress-plugin and lenso-http-egress-plugin Plugins, and workspace CI fully verifies every package archive.

The independent-process benchmark compares transport-only Axum, the bridge, and the complete Lenso path without sharing a Tokio runtime between client and server. Filters keep short local runs reproducible:

LENSO_HTTP_BENCH_BODY_BYTES=0 \
LENSO_HTTP_BENCH_CONNECTIONS=8 \
cargo bench -p lenso-web-ingress-plugin --bench http_ingress_process

The Web execution profile adds the decision evidence needed before introducing a different Web scheduler:

cargo bench -p lenso-web-ingress-plugin --bench web_execution_profile

It runs the same echo and delayed handlers through bare Axum, policy-equivalent Axum transport, the bounded bridge, and the complete Lenso native path. The report includes repeated throughput, unloaded p50/p99 latency, sampled server CPU and RSS, and classified saturation outcomes. A two-lane Lenso fixture also reports per-lane request counts for one, two, and eight HTTP/1.1 keep-alive connections.

LENSO_HTTP_BENCH_REQUESTS, LENSO_HTTP_BENCH_CONNECTIONS, and LENSO_HTTP_BENCH_SERVER can reduce the matrix. Process CPU/RSS sampling uses the host ps command and prints unavailable when the host cannot provide it. Treat results as host-specific evidence: a work-stealing Web profile is only justified if repeated runs show a material user workload problem that cannot be fixed at the Ingress routing or operation-granularity boundary. The benchmark does not change the portable Kernel or imply live Instance migration.

Event hosts

lenso-web-ingress-plugin also exposes WebIngressEventFactory for an event-owned App using an event Driver. Disable the default native feature to compile the shared HTTP path for wasm32-unknown-unknown. The event factory uses the same Plugin descriptor, Plan-bound generated Endpoint clients, routing, credentials and middleware as the native listener. See the event ingress seam for the host integration contract, buffered-only scope and qualification limits.

lenso-http-egress-plugin exposes HttpEgressEventFactory for an explicitly injected event transport, with a workers feature and the JS-owned abortable Fetch bridge. It reuses the native exact-origin and transfer policy. See event HTTP Egress for supported configuration, per-event injection, cancellation and target qualification requirements.