http
v0.22.2Expose functions as HTTP endpoints — registers the `http` trigger type and serves matched routes.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64
exact versions are immutable; binary and bundle artifacts are digest-pinned.
full markdown
/workers/http.md?version=0.22.2. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- concurrency_request_limit: 1024
cors:
allowed_methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowed_origins:
- *
default_timeout: 30000
host: 127.0.0.1
port: 3111
webhook_listener: nulldependencies
readme
http
Exposes registered functions as HTTP endpoints. Any function bound to an
http trigger becomes a route — external clients call it with a plain HTTP
request instead of going through the iii SDK, which is what you want for
webhooks, browser clients, or any caller outside the engine's WebSocket
protocol. Routing, path params, per-route/global middleware, conditional
execution, CORS, and chunked streaming responses are all handled by this
worker; the function just receives an HttpRequest and returns a value.
Install
iii trigger compose::add worker=httpiii trigger compose::add resolves the worker and its dependencies, writes
exact declarations to worker-compose.yaml, and reconciles the Compose project.
Quickstart
Register a function and bind it to this worker's trigger type (http — see
Trigger type) with api_path and http_method:
use iii_sdk::protocol::RegisterTriggerInput;
use iii_sdk::{InitOptions, RegisterFunction, errors::Error, register_worker};
use iii_http::types::HttpRequest;
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let iii = register_worker("ws://localhost:49134", InitOptions::default());
iii.register_function(
"orders::get",
RegisterFunction::new_async(|req: HttpRequest| async move {
Ok::<Value, Error>(json!({
"status_code": 200,
"body": { "order_id": req.path_params.get("id") },
}))
}),
);
iii.register_trigger(RegisterTriggerInput {
trigger_type: "http".to_string(),
function_id: "orders::get".into(),
config: json!({ "api_path": "/orders/:id", "http_method": "GET" }),
metadata: None,
})?;
tokio::signal::ctrl_c().await?;
Ok(())
}curl http://localhost:3111/orders/42
# {"order_id":"42"}The function's return value becomes the response: status_code (default
200), headers, and body. Path segments prefixed with : (e.g. :id)
land in req.path_params; query string and headers arrive as
req.query_params / req.headers.
Configuration
| Field | Default | Description |
|---|---|---|
port |
3111 |
TCP port the HTTP server binds to. 0 binds an OS-assigned ephemeral port. |
host |
0.0.0.0 |
Host/interface to bind. |
webhook_listener |
null (disabled) |
Optional restricted listener; only explicitly opted-in routes. |
webhook_listener.host |
127.0.0.1 |
Restricted listener interface; does not inherit host. |
webhook_listener.port |
3112 |
Restricted listener port; separate from the normal/admin listener on 3111. |
default_timeout |
30000 (ms) |
Per-request timeout; on expiry the server returns 504. |
cors.allowed_origins |
[] (permissive) |
Allowed CORS origins. An empty list allows any origin. |
cors.allowed_methods |
[] (permissive) |
Allowed CORS methods. An empty list allows any method. |
concurrency_request_limit |
1024 |
Maximum in-flight requests; requests over the limit wait for a slot. |
middleware[] |
[] |
Global middleware, each { function_id, phase, priority }, run on every route in ascending priority order before the handler. |
Configuration is owned by the configuration worker — edit it from the
console (Configuration → Workers → http) or seed it once via
--config on first boot. middleware and default_timeout
hot-reload without a restart, as do cors and concurrency_request_limit.
Changing host/port or webhook_listener binds the new addresses before
stopping the old listeners. All required binds must succeed; otherwise both
old listeners and their live configuration remain unchanged. Set
webhook_listener: null to disable it live. Shutdown drains both listeners
and serializes with reload; a late reload cannot restart a stopped server.
Trigger type
This worker always registers the http trigger type. Bind a function to it
with:
| Field | Required | Default | Description |
|---|---|---|---|
api_path |
yes | — | Route path, e.g. /orders/:id. Segments prefixed with : are extracted into path_params. |
http_method |
no | GET |
HTTP method to match. |
public_webhook |
no | false |
Also expose this route on the restricted listener. Omitted/false routes are normal-listener only. |
condition_function_id |
no | — | Function invoked first; if it returns a falsy value the request is rejected with 422. |
middleware_function_ids |
no | [] |
Per-route middleware, invoked before the handler in list order, in addition to any global middleware. |
Functions can stream their response: write to req.response (a
StreamChannelRef) with a ChannelWriter to send set_status /
set_headers control frames and body chunks for a chunked HTTP response.
Returning a non-null value instead yields a regular buffered response built
from { status_code, headers, body }.
Restricted webhook listener
Enable the optional listener in the existing http configuration:
host: 127.0.0.1
port: 3111
webhook_listener:
host: 127.0.0.1
port: 3112The internal http::webhook-listener::status function distinguishes saved
configuration from a listener that this process actually opened. Its applied
field contains the host from the applied configuration and the actual port from
the bound socket (including an OS-assigned port when configured with 0), or
null when the listener is off. The
last_reload_error field reports the most recent failed reload and is cleared
by the next successful one. The function waits for an in-progress reload
before responding and exposes no other HTTP configuration.
Register an explicit opt-in using the same http trigger provider:
{"api_path":"/webhooks/github","http_method":"POST","public_webhook":true}Only that opted-in route is eligible on 3112; it remains available on 3111.
Do not expose 3111 through a public proxy/tunnel. The restricted listener
mounts no Console, engine, or admin endpoints. Its private paths return 404
(including OPTIONS); methods not opted in for an eligible path return 405
with only public methods in Allow. Admission runs before CORS. It performs
no URL decoding, repeated/trailing-slash normalization, HEAD-to-GET aliasing,
or method-override handling. Parameter routes are still supported: opting
one in deliberately exposes every value matched by its parameter segments,
never a more specific private handler. Existing normal-listener behavior is
unchanged. Route removal or opt-in revocation applies to subsequent requests.
Both listeners share the route registry, configuration and layered router; global/per-route middleware, conditions, timeouts, concurrency and the 16 MiB body limit still apply. A bind collision, including using the normal address for the restricted listener, fails safely rather than combining them.
Signature verification: read HttpRequest.request_body using the SDK
ChannelReader; it contains the exact received bytes (UTF-8, JSON whitespace,
newlines and binary data included). Never compute HMAC from body, which is
only a parsed JSON convenience projection. Header names follow HTTP's
case-insensitive/lowercase representation; ASCII values such as
x-hub-signature-256 and x-github-delivery pass through unchanged. The
existing header map is not a raw HTTP-header archive: duplicate headers and
non-text values are not represented byte-for-byte. This listener does not
validate signatures itself; the opted-in handler must authenticate deliveries.
Local tests
tests/webhook_listener.rs uses the shared harness's loopback fake engine
and real SDK binary channels, without any external service or skipped tests:
CARGO_TARGET_DIR=target-http-local cargo test --locked --test webhook_listener
cargo fmt --all -- --check
CARGO_TARGET_DIR=target-http-local cargo clippy --all-targets --all-features --locked -- -D warningsOther e2e_* suites retain their existing connect-or-skip engine requirement.
Requires removing the legacy built-in HTTP service
The legacy built-in HTTP service also owns the http trigger type. Two owners
of the same trigger type on one engine collide — whichever registers last
wins — so this worker requires it to be absent: omit it from the
engine's config.yaml (a config that doesn't list a worker won't run it).
On boot, this worker queries the engine for connected workers and refuses to
start with a clear error if the legacy built-in is still active, so a stale config
fails loudly instead of silently racing the built-in worker for ownership of
http.
skills
api reference (json)
{
"functions": [
{
"description": "Returns this HTTP instance's configuration entry ID, without its value.",
"metadata": {
"internal": true
},
"name": "http::configuration-id",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ConfigurationIdentityRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"title": "ConfigurationIdentityResponse",
"type": "object"
}
}
],
"triggers": [
{
"description": "HTTP API trigger",
"invocation_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Configuration carried by an `http` trigger instance, as read from the trigger's `config` value.",
"properties": {
"api_path": {
"type": "string"
},
"condition_function_id": {
"default": null,
"type": [
"string",
"null"
]
},
"http_method": {
"default": "GET",
"type": "string"
},
"middleware_function_ids": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"public_webhook": {
"default": false,
"description": "Also expose this route on the restricted webhook listener. Explicit opt-in only; the default keeps existing routes on the normal listener.",
"type": "boolean"
}
},
"required": [
"api_path"
],
"title": "HttpTriggerConfig",
"type": "object"
},
"metadata": {},
"name": "http",
"return_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ChannelDirection": {
"enum": [
"read",
"write"
],
"type": "string"
},
"StreamChannelRef": {
"properties": {
"access_key": {
"type": "string"
},
"channel_id": {
"type": "string"
},
"direction": {
"$ref": "#/definitions/ChannelDirection"
}
},
"required": [
"access_key",
"channel_id",
"direction"
],
"type": "object"
},
"TriggerMetadata": {
"description": "Metadata describing the trigger that produced an `HttpRequest`.",
"properties": {
"method": {
"type": [
"string",
"null"
]
},
"path": {
"type": [
"string",
"null"
]
},
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
},
"description": "Request payload sent to the invoked function over the wire.",
"properties": {
"body": true,
"headers": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"method": {
"type": "string"
},
"path": {
"type": "string"
},
"path_params": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"query_params": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"request_body": {
"$ref": "#/definitions/StreamChannelRef"
},
"response": {
"$ref": "#/definitions/StreamChannelRef"
},
"trigger": {
"anyOf": [
{
"$ref": "#/definitions/TriggerMetadata"
},
{
"type": "null"
}
]
}
},
"required": [
"body",
"headers",
"method",
"path",
"path_params",
"query_params",
"request_body",
"response"
],
"title": "HttpRequest",
"type": "object"
}
}
]
}