state_stores: local: namespace: hookway memory: {} # Per process; select Redis or PostgreSQL for shared protection.# examples/on_error.yaml## on_error is a reserved output port on nodes that can fail with a recoverable# input. If the node returns an error and an on_error edge exists, the flow# continues with that node's *input* (not its output) and the request succeeds# unless the error path itself fails.# Protection store failures/capacity/policy conflicts always fail closed with# 503. Quota/duplicate/missing-key recovery may render local responses, but may# never dispatch publish, HTTP, registration or child work. A replay guard is# retained after any earlier successful or uncertain outbound effect, including# an HTTP attempt followed by DLQ recovery.## Type rule: the error-path target must consume the failing node's input port.# http_trigger, read_body, and no_auth cannot declare on_error. A node still# needs a success path; on_error-only wiring is rejected at validate.## Without on_error, the failure becomes the HTTP status from the node# (401, 413, 429, 502, …) or 500. Filter on_no_match: drop is a successful# stop, not an error, so it does not take this port.## See docs/reference/configuration.md § Flows.server: addr: ":8080" max_body_bytes: 10485760destinations: primary: type: webhook webhook: url: "${WEBHOOK_URL}" dlq: type: webhook webhook: url: "${WEBHOOK_URL}"flows: # Ingest: POST the decoded event to an ack URL. 5xx from that URL publishes # the original envelope to dlq instead of failing the webhook (sender sees # 202 if the DLQ publish succeeds). github_ack_or_dlq: nodes: - {id: in, type: http_trigger, description: "GitHub posts webhook deliveries here.", path: /webhooks/github, methods: [POST]} - {id: rl, type: rate_limit, store: local, description: "Caps inbound traffic before any body is read.", rpm: 60} - {id: body, type: read_body, description: "Reads the raw body, rejecting anything over 1 MiB.", max_bytes: 1048576} - {id: auth, type: auth, description: "Verifies the GitHub HMAC signature over the raw body.", scheme: hmac_sha256_hex, secret: "${GITHUB_WEBHOOK_SECRET}", signature_header: X-Hub-Signature-256} - {id: env, type: envelope, description: "Builds the JSON event envelope consumers receive.", body_format: json} - {id: ack, type: http_request, description: "Posts the event to the ack URL; transient failures retry, then a 5xx takes the error port.", url: "https://example.test/hooks", timeout: 5s, error_on_status: [5xx], retry: {max_attempts: 3, initial_backoff: 500ms, max_backoff: 5s}} - {id: out, type: publish, description: "Normal path: deliver the acknowledged envelope.", destination: primary, target: github-webhooks} - {id: dlq, type: publish, description: "Error path: park the envelope so the sender still gets a 202.", destination: dlq, target: github-failed} edges: [in>rl, rl>body, body>auth, auth>env, env>ack, ack>out, ack:on_error>dlq] # Sync proxy: upstream timeout or 5xx becomes an explicit 502 to the sender # instead of a 500 from an unhandled node error. proxy_or_502: nodes: - {id: in, type: http_trigger, description: "Accepts any method under the proxy prefix.", path: "/proxy/{rest...}", methods: [ANY]} - {id: rl, type: rate_limit, store: local, description: "Caps proxy traffic before any body is read.", rpm: 60} - {id: body, type: read_body, description: "Buffers the body so it can be forwarded upstream.", max_bytes: 10485760, allow_empty: true} - {id: auth, type: auth, description: "Compares the inbound X-Auth header to the shared token.", scheme: exact, secret: "${PROXY_TOKEN}", signature_header: X-Auth} - {id: env, type: envelope, description: "Leaves the body unparsed so the proxy stays byte-exact.", body_format: raw} - {id: up, type: http_request, description: "Forwards upstream; a timeout or 5xx takes the error port.", url: "${UPSTREAM_BASE}/{rest}", forward_method: true, forward_query: true, remove_headers: [X-Auth], timeout: 5s, error_on_status: [5xx]} - {id: resp, type: respond, description: "Normal path: return the upstream response unchanged.", passthrough: true} - {id: fail, type: respond, description: "Error path: an explicit 502 instead of an unhandled 500.", status: 502, body: "upstream failed"} edges: [in>rl, rl>body, body>auth, auth>env, env>up, up>resp, up:on_error>fail] # Rate limit: on_error carries RequestHead. The fallback still has to read # the body before it can respond 429 instead of the default rate-limit status. rate_or_429: nodes: - {id: in, type: http_trigger, description: "Endpoint with a deliberately tight rate limit.", path: /webhooks/limited, methods: [POST]} - {id: rl, type: rate_limit, store: local, description: "One request per burst; the rest take the error port.", rpm: 60, burst: 1, key_by: source_ip} - {id: body, type: read_body, description: "Normal path: read the accepted request body.", max_bytes: 1048576} - {id: auth, type: no_auth, description: "No signature to check on this endpoint."} - {id: env, type: envelope, description: "Builds the JSON envelope for the accepted request.", body_format: json} - {id: ok, type: respond, description: "Normal path: acknowledge with 200.", status: 200, body: ok} - {id: body2, type: read_body, description: "Error path: the 429 branch must read the body itself.", max_bytes: 1048576} - {id: na2, type: no_auth, description: "Error path: marks the request verified so envelope can run."} - {id: env2, type: envelope, description: "Error path: envelope needed before respond can be reached.", body_format: json} - {id: limited, type: respond, description: "Error path: an explicit 429 for the shed request.", status: 429, body: "rate limited"} edges: [in>rl, rl>body, body>auth, auth>env, env>ok, rl:on_error>body2, body2>na2, na2>env2, env2>limited] # Auth failure: on_error carries RawRequest. no_auth marks it verified so # envelope can decode and the sender gets 401 instead of the default auth status. auth_or_401: nodes: - {id: in, type: http_trigger, description: "Endpoint expecting a signed payload.", path: /webhooks/signed, methods: [POST]} - {id: body, type: read_body, description: "Reads the raw body so the signature can be checked.", max_bytes: 1048576} - {id: auth, type: auth, description: "Verifies the HMAC; a bad signature takes the error port.", scheme: hmac_sha256_hex, secret: "${GITHUB_WEBHOOK_SECRET}", signature_header: X-Hub-Signature-256} - {id: env, type: envelope, description: "Builds the JSON envelope for the verified request.", body_format: json} - {id: ok, type: respond, description: "Normal path: acknowledge with 200.", status: 200, body: ok} - {id: na, type: no_auth, description: "Error path: marks the request verified so envelope can run."} - {id: env2, type: envelope, description: "Error path: envelope needed before respond can be reached.", body_format: json} - {id: unauth, type: respond, description: "Error path: an explicit 401 for the bad signature.", status: 401, body: "unauthorized"} edges: [in>body, body>auth, auth>env, env>ok, auth:on_error>na, na>env2, env2>unauth] # Envelope parse failure: on_error carries Verified. A second envelope with # body_format: raw always succeeds, then respond 400. parse_or_400: nodes: - {id: in, type: http_trigger, description: "Endpoint expecting a JSON payload.", path: /webhooks/json, methods: [POST]} - {id: body, type: read_body, description: "Reads the raw body, rejecting anything over 1 MiB.", max_bytes: 1048576} - {id: auth, type: no_auth, description: "No signature to check on this endpoint."} - {id: env, type: envelope, description: "Parses JSON; malformed bodies take the error port.", body_format: json} - {id: ok, type: respond, description: "Normal path: acknowledge with 200.", status: 200, body: ok} - {id: raw, type: envelope, description: "Error path: raw format always parses, so respond is reachable.", body_format: raw} - {id: bad, type: respond, description: "Error path: an explicit 400 for the malformed body.", status: 400, body: "invalid json"} edges: [in>body, body>auth, auth>env, env>ok, env:on_error>raw, raw>bad]