REST Automation¶
Expose HTTP endpoints by configuration: rest.yaml is the router. An endpoint entry maps
a URL and its methods to a composable function (or an Event Script flow) — there are no
controllers, no handler registrations, and no web framework. The engine
(crates/platform-core/src/automation/) serves the routing table directly over hyper, so the
route string in the YAML is the only link between the HTTP edge and your code.
rest:
- service: "greeting.api"
methods: ['GET']
url: "/api/greeting/{user}"
timeout: 10s
cors: cors_1
headers: header_1
tracing: true
That entry — from the shipped examples/hello-world
— is the whole HTTP layer for the greeting endpoint. The function behind it is addressed only by
its route name, exactly as it would be from any other function.
A rest.yaml file has three sections: the rest endpoint list, reusable cors blocks, and
reusable headers transform blocks, plus an optional static-content block. Invalid entries
fail the load — a misconfigured endpoint is a startup error, not a silent 404.
Turning it on¶
REST automation is optional. Enable it in application.yml:
rest.automation¶
| Type | Default |
|---|---|
| boolean | false |
Starts the HTTP server at boot. The server also starts automatically when the application registers any websocket service, even if this flag is off.
rest.server.port¶
| Type | Default |
|---|---|
| int | 8085 |
The listen port. Port 0 binds an ephemeral port — useful in tests; the assigned address is
recoverable via platform_core::automation::server_address().
yaml.rest.automation¶
| Type | Default |
|---|---|
| string | classpath:/rest.yaml |
Location of the routing configuration (classpath:/ or file:/).
Rust port
The Java engine accepts a comma-separated list of locations here and merges them
sequentially; this port reads a single location. The Java server.port alias (for
Spring Boot co-existence) is also not read — there is no Spring in the Rust port, so
rest.server.port is the one key.
At startup every loaded route is logged as METHOD /url -> service, so the effective routing
table is always visible in the application log.
A rest: endpoint entry¶
service¶
| Type | Required |
|---|---|
| string | yes |
The route name of the composable function that handles matched requests. Use the built-in
http.flow.adapter together with flow to bind the endpoint to an Event Script flow
instead.
Rust port
Two Java service forms are deferred: an http(s)://… URL (HTTP relay, with its
url_rewrite and trust_all_cert companions) is rejected at load time with an explicit
"not yet ported" error, and a two-element list (A/B dual service — primary plus a
listen-only secondary) fails to load because service must be a single route. Multipart
upload (the Java upload flag) is also deferred; unrecognized keys in an entry are ignored.
methods¶
| Type | Required |
|---|---|
| list | yes |
Any subset of GET, PUT, POST, DELETE, HEAD, PATCH. Anything else fails the load.
Do not list OPTIONS — it is added automatically for CORS preflight and matches every entry.
url¶
| Type | Required |
|---|---|
| string | yes |
The URI path. Literal segments match case-insensitively. Two dynamic forms are supported:
{param}captures one path segment (case preserved) and delivers it to the function as a path parameter —/api/greeting/{user};- a trailing
*matches the remainder of the path (it must be the last segment) —/api/files/*.
When several entries match, the most specific wins: a non-wildcard route beats a wildcard one,
then the route with more literal segments wins — so /api/greeting/system is chosen over
/api/greeting/{user} for that exact path.
timeout¶
| Type | Default |
|---|---|
| duration | 30s |
Maximum time to wait for the function's response, written as 10s, 2m, or 1500ms (a bare
number means seconds). Values are clamped to the range 1 second – 5 minutes; an unparsable
value falls back to the default. On expiry the caller receives an HTTP 408 error response.
For a flow-bound endpoint this value also rides into the flow input dataset as the timeout
field (in seconds), from which the flow adapter derives the flow instance TTL.
cors¶
| Type | Default |
|---|---|
| string | none |
The id of a cors block. The reference must exist or the load fails.
headers¶
| Type | Default |
|---|---|
| string | none |
The id of a headers block. The reference must exist or the load fails.
authentication¶
| Type | Default |
|---|---|
| string | none |
The route name of an authentication function. When set, the incoming request event is first sent to this route (with the endpoint's timeout). The contract:
- return body
true→ the request proceeds toservice; - return body
false(or anything that is not booleantrue) → 401 Unauthorized; - return an error (
Err(AppError)/ status ≥ 400) → that status and message are returned to the caller, so the function can reject with a custom status such as 403; - headers on the verdict envelope become session info that rides to the target
function as read-only headers (the
sessionmap of theAsyncHttpRequest) — e.g. put the validated user id there. A working example is theevent.api.authdemo in the Event over HTTP guide.
Rust port
Only the simple form — one route for the endpoint — is ported. The Java routing specs
('default: svc', 'header: svc', 'header: value: svc') are not yet available.
tracing¶
| Type | Default |
|---|---|
| boolean | false |
Starts a distributed trace at the HTTP edge for every request on this endpoint. The trace id
is resolved in precedence order: a well-formed W3C traceparent header wins (and the caller's
span becomes the parent span); otherwise the trace-id header (X-Trace-Id by default);
otherwise a fresh id is generated. See
Function Execution — tracing
for what happens downstream.
trace.id.header / correlation.id.header / traceparent.header¶
| Type | Default |
|---|---|
| string | global config |
Per-endpoint overrides of the header names used to capture the trace id and the business
correlation id — impedance matching for one legacy caller with its own conventions. Precedence
is per-entry override, then the application.yml globals (http.trace.id.header, default
X-Trace-Id; http.correlation.id.header, default X-Correlation-Id), then the built-in
default. Header capture is case-insensitive, and a well-formed W3C traceparent still takes
precedence for the trace id.
traceparent.header overrides the global http.traceparent.header (default traceparent)
— the header carrying the full W3C trace context, for backward compatibility with a
legacy intermediary only (departure from the W3C/OTel standard is discouraged); the
standard traceparent always wins, and the custom name is read only when the standard is
absent. See Observability for the standards
position and the semantics.
A business correlation id is always ensured — independent of tracing. If the caller sends
none, one is generated (when one header name serves both roles, the trace id is reused so a
legacy caller sees one id, not two). It is exposed to the target function as the read-only
my_correlation_id request header, and to a flow as model.cid.
flow¶
| Type | Default |
|---|---|
| string | none |
An Event Script flow id, used together with service: http.flow.adapter. The complete
flow-bound endpoint from examples/hello-flow:
rest:
- service: "http.flow.adapter"
methods: ['GET']
url: "/api/hello/{user}"
flow: 'hello-flow'
timeout: 10s
tracing: true
REST automation injects the flow id as the x-flow-id request header; the flow adapter
reshapes the HTTP request into the flow's input.* dataset and launches the flow, whose final
output.* mappings become the HTTP response. See the
Flow Schema Reference and the
flow grammar.
cors: blocks¶
Reusable CORS configurations, referenced from an entry by id. Convenient in development;
production systems usually do CORS at the API gateway.
cors:
- id: cors_1
options:
- "Access-Control-Allow-Origin: *"
- "Access-Control-Allow-Methods: GET, OPTIONS"
- "Access-Control-Allow-Headers: Content-Type, X-Correlation-Id, traceparent"
headers:
- "Access-Control-Allow-Origin: *"
options lines become the response headers of the preflight: an OPTIONS request to any
matching URL is answered directly by the engine with 204 No Content plus these headers —
it never reaches your function. headers lines are added to every normal response from
the endpoint. Every line in both lists must have the form Access-Control-*: value; anything
else fails the load.
headers: blocks¶
Reusable request/response header transforms, referenced from an entry by id.
headers:
- id: header_1
request:
drop: ['upgrade-insecure-requests', 'cache-control']
response:
add:
- "x-served-by: mercury-rust"
Both request and response accept three directives, applied in this order:
keep- Retain only the listed header names, dropping everything else. When
keepis non-empty it takes precedence overdrop. drop- Remove the listed header names.
add- Insert
'name: value'lines (after keep/drop, so an added header always survives).
Header-name matching is case-insensitive throughout.
What your function receives¶
A matched request is delivered as an event whose body is the AsyncHttpRequest map — the same shape the Java engine uses:
{
"method": "GET",
"url": "/api/greeting/eric",
"ip": "127.0.0.1",
"https": false,
"host": "127.0.0.1:8085",
"headers": { "accept": "*/*", "my_correlation_id": "…" },
"parameters": {
"path": { "user": "eric" },
"query": { "lang": "fr" }
},
"body": null,
"timeout": 10
}
Header names arrive lowercase. Path and query parameter values are percent-decoded (+
becomes a space). timeout is the endpoint's timeout in seconds.
The idiomatic way to consume it is a typed function: declare
#[preload(..., typed)] + impl TypedFunction<AsyncHttpRequest, O> and the dataset
deserializes into the request model — request.path_parameter("user"),
request.query_parameter("lang"), request.header("accept"), request.cookie(...),
request.body_as::<T>() (Java TypedLambdaFunction<AsyncHttpRequest, Object> parity). An
untyped ComposableFunction sees the same map on the raw envelope. See
Write Your First Function for the full
example.
Content-type dispatch — how the body is parsed¶
The request body is parsed by the declared content-type (any ;charset=… suffix is
ignored; the value is matched case-sensitively, Java parity). This port mirrors the Java
HttpRouter dispatch exactly:
application/json- An empty body becomes an empty map
{}. A body wrapped in matching JSON brackets ({…}or[…]) is parsed into a map or list; if that parse fails, the raw text is delivered instead of an error. A body without the wrapping brackets stays raw text. There is no JSON sniffing under any other content type — a JSON-looking body sent astext/plainarrives as text. application/xml- Delivered as raw text — see the port note below.
application/x-www-form-urlencoded- Form fields are decoded into query parameters (merged with any URL query string); the body itself stays null.
text/html,text/plain- Delivered as text.
- anything else, or no content type
- Delivered as raw bytes on the event envelope (an empty payload stays null). Bodies
with or without a
content-lengthheader are aggregated in full before dispatch.
Rust port
The XML-to-map parse is deferred with the rest of the XML surface (the Java
SimpleXmlParser/writer pair is not ported) — XML bodies pass through as raw text on both
the server and the HTTP-client side. Request/response streaming is also deferred:
bodies are aggregated, matching Java's fixed-length path. The Java custom.content.types
mapping feature is not ported.
The response¶
The function's reply envelope maps back to HTTP:
- Status — the envelope status (HTTP semantics; ≥ 400 is an error).
- Body and content type — a function-set
content-typeenvelope header always wins; otherwise the fallback is negotiated from the request'sAcceptheader (Java parity):text/htmlrenders a map/list body as JSON wrapped in an HTML shell,application/jsonor*/*negotiate JSON, noAcceptsends no content-type header, anything else istext/plain; anapplication/xmlAccept negotiates JSON (the port's XML deferral). Strings and bytes always ride raw regardless of the negotiated type. Null map entries are omitted unlessserializer.null.transport=true. - Headers — the response transforms of the entry's
headersblock are applied, then the entry's CORSheaderslines are added.
Errors — a missing route, a rejected authentication, a function error, or a timeout — share one JSON shape (Java parity):
with the matching HTTP status: 404 for an unmatched URL, 401 from authentication,
408 when the endpoint timeout expires, and the function's own error status otherwise.
Static content¶
When no rest.yaml entry claims a URL, GET/HEAD requests fall through to static content
served from the application's resources/public folder (an entry in rest.yaml always wins
over a static file). / and any path ending in / resolve to index.html; an extensionless
filename assumes .html; parent-directory traversal is rejected. Content types are resolved
from the file extension.
Rust port
The Java static.html.folder parameter is not read — static content always comes from
the application's resources/public. The mime-types.yml customization is deferred; a
built-in extension table covers the common web types.
The optional static-content block tunes the behavior — this is the shipped
examples/hello-world configuration:
static-content:
no-cache-pages: ["/", "/index.html"]
filter:
path: ["/", "/assets/*", "*.html", "*.js"]
exclusion: ["*.css"]
service: "http.request.filter"
no-cache-pages¶
| Type | Default |
|---|---|
| list | ["/", "/index.html"] |
Pages served with Cache-Control: no-cache, no-store (plus Pragma/Expires) instead of the
etag protocol — entry pages must always revalidate, e.g. for SSO redirection. Everything else
gets an ETag (a SHA-256 content hash); a matching If-None-Match returns 304 with an
empty body. Patterns here and in the filter are exact, prefix*, or *suffix.
filter¶
| Type | Default |
|---|---|
| map | none |
A composable function (service) that inspects static requests matching path but not
exclusion — the typical use is cookie checks and SSO browser redirection, or adding security
response headers. The filter receives an AsyncHttpRequest-shaped event (no body); its response
headers are always copied onto the HTTP response. Status 200 lets the file be served; any
other status — e.g. a 302 with a Location header — passes the filter's response through to
the client instead. path and service are mandatory when a filter is configured.
Rust port
If the filter function itself fails, this port logs the error and serves the file anyway — a resilient divergence from Java, which leaves the request to time out.
Built-in endpoints¶
When rest.yaml does not claim their URLs, five endpoints are added automatically: the four
actuators /info, /env, /health, /livenessprobe, and POST /api/event (Event over
HTTP). Your own entry for one of these URLs always wins, which is how you attach
authentication, CORS, or a different timeout to them. See
Actuators & HTTP Client and
Event over HTTP.
Rust port
Two Java defaults remain unported — /info/lib and /info/routes — see the
actuator port notes.
See also¶
- Getting Started — run the examples these snippets come from.
- Actuators & HTTP Client — the built-in endpoints and the outbound async HTTP client.
- Flow Schema Reference — the flows that
flow:endpoints launch. - Write Your First Function — handling the AsyncHttpRequest event.
Adapted from the mercury-composable guides docs/guides/rest-automation/index.md and
docs/guides/rest-automation/rest-grammar.md; behavior verified against this repository's
source.