Skip to content

Request flow

This page owns the request sequence for POST /proxy/{scheme}/{host}/{*rest} (crates/mint-broker/src/proxy.rs:29-35). ContainmentRule, Decision, and AuditEvent are defined on Concepts.

The initial readiness check can return 503 without an event because the audit sink is unavailable; no effect is attempted. After readiness passes, every early-return branch below attempts to append an AuditEvent before responding. If that append fails, the gate becomes unhealthy and later effects fail closed, but that refused request may be absent from the audit log (crates/mint-broker/src/audit_gate.rs:397-407).

sequenceDiagram
    autonumber
    participant Agent
    participant Handler as proxy_handler
    participant Gate as AuditGate
    participant Policy
    participant Task as run_prepared_effect<br/>(detached task)
    participant Store as SecretStore
    participant Vendor

    Agent->>Handler: HTTP request, placeholder in header/body
    Handler->>Gate: is_ready()
    alt gate not ready (draining or unhealthy)
        Handler-->>Agent: 503, no effect attempted (proxy.rs:48-64)
    else ready
        Handler->>Handler: reject_ambiguous_ingress(uri, headers)
        alt absolute-form target, path traversal, control byte
            Handler->>Gate: append(Deny, ingress_boundary_rejected)
            Handler-->>Agent: 400 bad request (proxy.rs:72-82)
        else ingress clean
            Handler->>Handler: authenticate(peer, headers) on spawn_blocking
            alt no/invalid capability, whois miss, task panic
                Handler->>Gate: append(Deny, auth_failed)
                Handler-->>Agent: 401 (proxy.rs:100-124)
            else actor resolved
                Handler->>Policy: evaluate(EvalRequest)
                alt Decision::Deny or scheme not permitted
                    Handler->>Gate: append(Deny, policy/scheme reason)
                    Handler-->>Agent: 403 (proxy.rs:142-158)
                else Decision::Allow
                    Handler->>Handler: resolve_target(host); block private egress unless opted in
                    alt DNS fails, or private/loopback IP without allow_private_network
                        Handler->>Gate: append(Deny, target_resolution_failed / private_egress_not_permitted)
                        Handler-->>Agent: 502 / 403 (proxy.rs:160-183)
                    else target resolved
                        Handler->>Handler: call_guard.check_and_increment(actor, host)
                        alt call-guard window exceeded
                            Handler->>Gate: append(Deny, call_guard_exceeded)
                            Handler-->>Agent: 429 (proxy.rs:207-218)
                        else within guard
                            Handler->>Handler: breaker.check(host)
                            alt circuit open
                                Handler->>Gate: append(Deny, circuit_breaker_open_until)
                                Handler-->>Agent: 503 (proxy.rs:221-226)
                            else circuit closed
                                opt rule sets require_placeholder
                                    Handler->>Handler: scan query/body for a literal credential
                                    alt literal credential present, no placeholder
                                        Handler->>Gate: append(Deny, require_placeholder_violation)
                                        Handler-->>Agent: 403 (proxy.rs:228-259)
                                    end
                                end
                                Handler->>Gate: prepare(event)
                                alt prepare fails (audit sink down)
                                    Handler-->>Agent: 503, no effect attempted (proxy.rs:261-267)
                                else prepared, durably recorded
                                    Handler->>Task: spawn, latched on a oneshot
                                    Handler->>Gate: track(effect_id, abort_handle)
                                    Note over Handler,Task: latch releases only after track()<br/>succeeds -- a concurrent drain can<br/>never let the task reach the vendor<br/>after a forced terminal (proxy.rs:279-314)
                                    Note over Task: an unpermitted alias in a body or query value<br/>stays inert literal text, never resolved (proxy.rs:536-550,659-668)
                                    Task->>Store: resolve each header alias, and each permitted body/query alias, bounded timeout
                                    alt header alias not permitted by rule
                                        Task->>Gate: finalize_prepared(Deny, alias_not_permitted_by_rule)
                                        Task-->>Handler: 403 (proxy.rs:403-421)
                                    else a resolve call fails or times out
                                        Task->>Gate: finalize_prepared(Deny, secret_resolve_failed)
                                        Task-->>Handler: 502 (proxy.rs:422-437)
                                    else every alias resolved
                                        Task->>Vendor: forward with secret substituted, Accept-Encoding: identity
                                        alt connect/TLS/timeout error
                                            Task->>Gate: finalize_prepared(Indeterminate, upstream_error/upstream_timeout)
                                            Task-->>Handler: 502 / 504 (proxy.rs:768-796)
                                        else response received
                                            Task->>Task: decode body, scrub_response_body(), scrub headers
                                            alt unsupported/stacked encoding, oversized, unreadable
                                                Task->>Gate: finalize_prepared(Deny/Indeterminate, response_* reason)
                                                Task-->>Handler: 502 / 504 (proxy.rs:798-1128)
                                            else body decoded and scrubbed
                                                alt rule sets never_relay_credentials AND a credential is still detected
                                                    Task->>Gate: finalize_prepared(Deny, credential_in_response_denied)
                                                    Task-->>Handler: 502, response withheld entirely (proxy.rs:1138-1161)
                                                else clean
                                                    Task->>Gate: finalize_prepared(Observed, Allow)
                                                    Task-->>Handler: 200 + scrubbed body/headers (proxy.rs:1163-1204)
                                                end
                                            end
                                        end
                                    end
                                    Handler-->>Agent: relay Task's response
                                end
                            end
                        end
                    end
                end
            end
        end
    end

The initial audit-readiness rejection is a third shape: it returns 503 without an audit write because the sink is unavailable, and no effect is attempted (crates/mint-broker/src/proxy.rs:48-64). After readiness passes, every alternative above ends with either a synchronous AuditGate::append for a decision made before prepare (crates/mint-broker/src/audit_gate.rs:261-267), or append_terminal_failure/finalize_prepared for a decision made after it (crates/mint-broker/src/proxy.rs:1338-1349). A never_relay_credentials rule additionally refuses to relay the response body at all rather than trust a best-effort scrub for a token-minting endpoint (crates/mint-core/src/policy.rs:163-177, enforced at crates/mint-broker/src/proxy.rs:1138-1161). An unpermitted alias in a header value is the only placeholder occurrence that denies the call; the same alias merely quoted in a body or a query value is left as inert literal text and never reaches this fail-closed machinery at all (crates/mint-broker/src/proxy.rs:536-550,659-668, see Security for the boundary this draws).

axum/hyper can drop a handler's future the instant the client disconnects, even after prepare() has durably recorded the effect (crates/mint-broker/tests/cancellation_safety.rs:1-11). proxy_handler defends against this by spawning run_prepared_effect as its own detached task, latched on a oneshot channel so it cannot start forwarding to a vendor before AuditGate::track has installed its abort handle (crates/mint-broker/src/proxy.rs:279-314). The handler only .awaits the task to relay its result if the connection is still there to receive it; the task itself runs to its own terminal record regardless (crates/mint-broker/src/proxy.rs:315-333). The regression test client_disconnect_after_prepare_does_not_abandon_the_effect drives a real TCP connection, drops it before reading any response, and asserts the effect still reaches exactly one terminal record (crates/mint-broker/tests/cancellation_safety.rs:72-171).

Graceful shutdown closes the remaining gap: SIGTERM/SIGINT stops new admission immediately, waits up to a bounded drain_bound for in-flight effects to reach a terminal record, and force-terminalizes anything still active as Indeterminate/Deny — never a fabricated success (crates/mint-broker/src/lib.rs:333-344, crates/mint-broker/src/audit_gate.rs:329-330).

ClaimSourceLines
Handler audit-readiness scopeproxy.rs29-35
Audit-not-ready branchproxy.rs48-64
Ingress boundary rejectionproxy.rs72-82
Authentication branchproxy.rs84-124
Policy/scheme denyproxy.rs134-158
Target resolution / private egressproxy.rs160-183
Call guardproxy.rs200-219
Circuit breakerproxy.rs221-226
require_placeholder violationproxy.rs228-259
prepare and detached spawnproxy.rs261-314
Header alias not permitted → 403proxy.rs403-421
Secret resolve failure → 502 (same pattern repeats for the body loop at L551-L570 and the query loop at L669-L688)proxy.rs422-437
Unpermitted alias in a body value left inert, no resolution attemptedproxy.rs536-550
Unpermitted alias in a query value left inert, no resolution attemptedproxy.rs659-668
Upstream transport error/timeoutproxy.rs768-796
Response decode/scrub-safety failuresproxy.rs798-1128
never_relay_credentials denialproxy.rs1138-1161
Terminal Allow write and responseproxy.rs1163-1204
append_terminal_failureproxy.rs1338-1349
Header vs. body/query placeholder split, rationaleinert_unpermitted_placeholder.rs1-18
Header boundary regression testinert_unpermitted_placeholder.rs346-389
AuditGate::prepare/appendaudit_gate.rs261-270
force_terminalize_activeaudit_gate.rs329-330
Cancellation-safety test rationalecancellation_safety.rs1-11
Cancellation-safety testcancellation_safety.rs72-171
Graceful drain, force-terminalizelib.rs333-344