Sign in

    moonbitstack/moonzero/rest/handler does not have a README file

    Middleware

    An AsgiApp transformer — one layer of the middleware onion.

    CorsConf

    pub(all) struct CorsConf {
    allow_origin : String
    allow_methods : String
    allow_headers : String
    allow_credentials : Bool
    max_age : Int
    }

    CORS configuration (← go-zero's cors.Middleware options): the values echoed back in the Access-Control-* preflight/response headers.

    CorsConf::new

    fn CorsConf::new(allow_origin? : String, allow_methods? : String, allow_headers? : String, allow_credentials? : Bool, max_age? : Int) -> CorsConf

    Build a permissive CORS config: any origin, the full method set, and a one-day preflight cache. Credentials are off by default, matching go-zero.

    Deadline

    pub struct Deadline {
    budget_ms : Int64
    started_ms : Int64
    }

    A request deadline (← go-zero's timeout middleware's context.WithTimeout): a budget in milliseconds measured from a start instant on the shared clock. A budget_ms <= 0 means "no deadline" and never expires — go-zero's convention for a disabled timeout.

    Deadline::expired

    fn Deadline::expired(self : Deadline, now : Int64) -> Bool

    Whether the deadline has passed at time now. A non-positive budget never expires.

    Deadline::remaining

    fn Deadline::remaining(self : Deadline, now : Int64) -> Int64

    Milliseconds left before the deadline at time now (never negative); -1 for a disabled (non-positive-budget) deadline, which has no finite remaining.

    Deadline::start

    fn Deadline::start(budget_ms : Int64, now : Int64) -> Deadline

    Start a deadline of budget_ms milliseconds at time now.

    Refusal

    pub(all) struct Refusal {
    status : Int
    body : Bytes
    headers : Array[(String, String)]
    }

    The response a refused request receives: a status, a body, and the headers sent with it.

    Refusal::events

    The refusal as the ASGI events that send it. A pure value, so a middleware's answer is testable without driving the async transport.

    Refusal::new

    fn Refusal::new(status? : Int, body? : Bytes, headers? : Array[(String, String)]) -> Refusal

    Build a refusal. Left alone it is the 503 Service Unavailable the breaker, the shedder and the connection limiter all answer with.

    RequestLog

    pub(all) struct RequestLog {
    http_method : String
    path : String
    status : Int
    duration_ms : Int64
    request_id : String
    client_ip : String
    user_agent : String
    }

    A structured access-log record (← go-zero's logx HTTP access fields). Rather than a free-form line, each request is captured as typed fields and rendered as one JSON object per line — the format go-zero emits under logx and the shape log collectors (ELK, Loki) expect.

    RequestLog::fields

    fn RequestLog::fields(self : RequestLog) -> Array[(String, Json)]

    The record as logx fields, for logging it through a @logx.Logger — the method and path make the entry's message, so they are not repeated here.

    RequestLog::render

    fn RequestLog::render(self : RequestLog) -> String

    The record rendered as a single-line JSON string, ready to write to a log sink.

    RequestLog::to_json

    fn RequestLog::to_json(self : RequestLog) -> Json

    The record as a JSON value with a stable field order, duration_ms rendered as a number of milliseconds.

    auth

    JWT auth middleware (← go-zero's handler.Authorize): require every HTTP request to carry a valid Authorization: Bearer <jwt> header. The token is verified against secret under HS256 at the current time read from clock (milliseconds, converted to the JWT seconds epoch); an absent, malformed, tampered, expired, or not-yet-valid token is rejected with 401 Unauthorized before the wrapped app runs. Non-HTTP scopes (lifespan, websocket) pass through untouched.

    breaker

    Circuit-breaker middleware (← go-zero's breaker interceptor): gate each HTTP request through a shared Breaker. An admitted request's outbound HttpResponseStart status is observed — a 5xx settles its promise as a failure, anything else as a success, and that is what feeds the window. A shed request is answered 503 Service Unavailable without the wrapped app running. Non-HTTP scopes pass through untouched.

    The promise starts out a failure and only the observed status makes it a success, on a defer — the same shape go-zero's doReq uses, so a handler that raises or is cancelled before answering counts against the window instead of quietly not counting at all.

    cors

    CORS middleware (← go-zero's cors.Middleware): wrap the outbound Send so the configured Access-Control-* headers are injected onto every HttpResponseStart, leaving the body and other events untouched.

    jwt_authorized

    fn jwt_authorized(token : String?, secret : String, now_secs : Int64) -> Bool

    Whether a request bearing token is authorised at now_secs: the token verifies against secret under HS256 and is neither expired nor not-yet-valid. A missing token is unauthorised. Exposed as a pure decision so the middleware's accept/reject is testable without driving the transport.

    jwt_sign

    fn jwt_sign(claims : Map[String, Json], secret : String, typ? : String?, extra? : Map[String, Json], wins? :
    Wins
    , clash? :
    OnClash
    [Map[String, Json]]) -> String

    Sign a claims set as a compact HS256 JWT (← go-zero's jwt.NewWithClaims(SigningMethodHS256, ...)).

    claims is the payload; put exp/iat/nbf/sub in it as ordinary entries. typ is the header's type ("JWT" unless given; None writes no typ header at all), and extra merges further members into the header.

    Configuration has three layers, later ones winning: the library preset < the typ given here < extra. wins turns the last two around and clash decides what a collision does — by default the reserved header fields we write are the ones that stand, and overwriting one raises.

    jwt_verify

    fn jwt_verify(token : String, secret : String, now_secs : Int64, policy? :
    Policy
    ) -> Map[String, Json] raise
    Rejected

    Verify a compact HS256 JWT and return its claims (← go-zero's handler.Authorize).

    now_secs is the verification time as a Unix timestamp in seconds, the unit JWT's NumericDate uses — not the milliseconds the [Clock] seam carries, because the claim is defined in seconds.

    policy carries every check: exp and nbf are verified by default and iat is not, which is what PyJWT and jjwt both do. Hand it a different Policy to require an issuer, an audience, or particular claims.

    logging

    A request-logging middleware: writes METHOD path through the process logx at info for each HTTP request, then delegates to the wrapped application. It logs on the way in and knows nothing of the response; structured_logging is the layer that times the request and records its status.

    max_conns

    Max-connections middleware (← go-zero's MaxConns): hold a permit for the wrapped app's duration, or answer 503 Service Unavailable when every permit is taken. Non-HTTP scopes (lifespan, websocket) pass through untouched.

    maxbytes

    Max-bytes middleware (← go-zero's MaxBytesHandler): reject any HTTP request whose declared Content-Length exceeds limit bytes with 413 Payload TooLarge, before the wrapped app runs. A limit <= 0 disables the check. Non- HTTP scopes pass through untouched.

    metrics

    Metrics middleware (← go-zero's prometheus interceptor): time each HTTP request on the shared clock and, when the response starts, count it under "<METHOD> <path> <status>" and record its latency in milliseconds. The record is taken once per request even if a downstream (under a recovery race) emits a second start. Non-HTTP scopes pass through unmeasured.

    metrics_handler

    A handler that serves the current metric set in the text exposition format, with the Prometheus content type — the scrape target go-zero's prometheus.StartAgent publishes at /metrics.

    mount_metrics

    Register the GET /metrics scrape endpoint on app (← go-zero's prometheus.StartAgent), so a Prometheus server can pull the exposition.

    period_limit

    Fixed-window rate-limit middleware over the process-local limiter: account for each HTTP request under key and answer 429 Too Many Requests once the key is over quota for the current window, otherwise delegating to the wrapped app. The limiter is captured once per assembly, so its windows are shared across every request this layer serves — but only within this process; use redis_period_limit to share them across replicas. Non-HTTP scopes pass through untouched.

    plain_text

    let plain_text : Array[(String, String)]

    The headers a refusal carries unless told otherwise.

    rate_limit

    Rate-limit middleware over the process-local bucket: admit each HTTP request against a shared TokenBucket read at clock.now(), answering 429 Too ManyRequests when the bucket is empty and otherwise delegating to the wrapped app. The bucket is captured once per assembly, so its state is shared across every request this layer serves — but only within this process; use redis_rate_limit to share it across replicas. Non-HTTP scopes (lifespan, websocket) pass through untouched.

    recovery

    Recovery middleware (← go-zero's RecoverHandler): run the wrapped application inside a try, and if it raises, emit a 500 Internal ServerError instead of letting the failure escape to the server. A downstream that has already streamed its response start before raising will produce a second start event; recovery is a last-resort guard, so it always answers rather than trying to detect that race.

    redis_period_limit

    Fixed-window rate-limit middleware over a shared redis: period_limit with the window in redis, so replicas behind one redis spend one quota between them. A redis that cannot be reached admits the request — the limiter is a guard on the service, not a dependency that should be able to close it.

    redis_rate_limit

    Rate-limit middleware over a shared redis: rate_limit with the bucket in redis, so replicas behind one redis spend from one bucket. A redis that cannot be reached leaves the limiter running on its local bucket, so the layer keeps limiting either way. Non-HTTP scopes pass through untouched.

    refusal

    let refusal : Refusal

    The generic refusal: 503 Service Unavailable.

    request_id

    Request-ID middleware (← go-zero's trace/x-request-id handling): reuse an inbound x-request-id if the client sent one, otherwise mint a fresh monotonic id, and stamp it onto every response's HttpResponseStart. The counter is captured once per assembly, so ids stay unique across the requests this layer serves.

    shedding

    Shedding middleware (← go-zero's SheddingHandler): answer 503 ServiceUnavailable without running the app while the shedder is refusing, and pass everything else through. Non-HTTP scopes are never shed.

    structured_logging

    Structured-logging middleware (← go-zero's LogHandler): time each HTTP request on the shared clock, capture its method/path/client-ip/user-agent and the response status observed on HttpResponseStart, and write one access entry through logger (the process logx by default) when the response starts. Access logs are info, so a service configured at error or above emits none of them. Non-HTTP scopes pass through without logging.

    timed_out

    let timed_out : Refusal

    What a request past its deadline answers with. go-zero's timeout middleware answers 503 with this body rather than 504, so that is the default.

    timeout

    Timeout middleware (← go-zero's TimeoutHandler): establish a per-request Deadline of budget_ms at clock.now() for the wrapped app.

    Async boundary (faithful model). Preemptively aborting an in-flight handler the instant its deadline fires requires racing the handler against a timer and cancelling the loser — in MoonBit that is @async.any([handler,timer]) with structured cancellation, which only runs under the native async runtime and cannot be driven synchronously. What this middleware does portably: it installs the deadline and enforces it on the response path — if the handler blows its budget before emitting its first event, the client receives a 503 timeout (from timeout_events) and the late response is suppressed. The remaining gap (a handler that hangs and never emits) is closed by the race/cancel wired at the async server edge. A budget_ms <= 0 disables the timeout, passing straight through.

    too_large

    let too_large : Refusal

    What an over-large request body answers with (← go-zero's maxBytes).

    too_many

    let too_many : Refusal

    What a rate-limited request answers with (← go-zero's periodlimit and tokenlimit handlers).

    tracing

    fn tracing(header? : String, ignore_paths? : Array[String]) -> ((async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit) -> (async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit))

    Trace-id propagation middleware (← go-zero's trace handler): continue the inbound traceparent trace or start a new one, mint a child span, and stamp both traceparent and a convenience x-trace-id onto the response so the id flows to the client and downstream calls. The per-assembly seed counter keeps span ids distinct across the requests this layer serves. Requests whose path is listed in ignore_paths are left untraced (← WithTraceIgnorePaths, the blacklist that keeps health checks out of the trace store), as are non-HTTP scopes.

    unavailable

    let unavailable : Refusal

    What an open breaker, a shed request and a spent connection budget answer with (← go-zero's breaker, sheddingHandler, maxConns).