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

    Middleware

    An AsgiApp transformer — one layer of the middleware onion.

    Decrypter

    pub(open) trait Decrypter {
    fn decrypt(Self, Bytes) -> Bytes raise
    }

    Turns the sealed secret field of the header into the envelope it carries. Implemented by the RSA decrypter below and open to any transport a caller prefers — an HSM, a KMS, a key in an environment variable.

    Failure

    pub(all) suberror Failure {
    Failure(String)
    }

    The one failure a decrypter reports: the sealed block is not one.

    Refused

    pub(all) suberror Refused {
    MissingHeader
    MalformedHeader
    UnknownKey
    BadSecret
    StaleTimestamp
    BadCiphertext
    BadSignature
    }

    A content-security failure. Each variant names the one check that refused the request, so a caller can tell a replay from a wrong key.

    AsLayer

    type AsLayer

    A middleware as a layer of the mid stack.

    CorsConf

    pub(all) struct CorsConf {
    origins : Array[String]
    methods : String
    headers : String
    expose : String
    max_age : Int
    }

    CORS configuration: which origins may call the service, and what a preflight is told about the rest of the policy.

    CorsConf::new

    fn CorsConf::new(origins? : Array[String], methods? : String, headers? : String, expose? : String, max_age? : Int) -> CorsConf

    A permissive CORS config: any origin, go-zero's method and header sets, and a one-day preflight cache.

    FileEntry

    pub(all) struct FileEntry {
    body : Bytes
    media_type : String
    }

    One servable file: its bytes and the media type it is announced with. The ETag is derived from the content, so a conditional request works without the loader having to know a modification time.

    FileEntry::etag

    fn FileEntry::etag(self : FileEntry) -> String

    The ETag of a file: a quoted, strong validator derived from the length and a CRC-32 of the bytes, so two builds that produce the same content serve the same tag.

    Files

    pub(all) struct Files {
    entries : Map[String, FileEntry]
    }

    The files a service serves, keyed by path relative to the mount prefix.

    Files::add

    fn Files::add(self : Files, path : String, body : Bytes, media_type? : String) -> Unit

    Add body under path (/logo.svg, say). The media type is guessed from the extension unless one is given.

    Files::get

    fn Files::get(self : Files, path : String) -> FileEntry?

    The file registered under path, or None.

    Files::new

    fn Files::new() -> Files

    An empty set of files.

    Files::paths

    fn Files::paths(self : Files) -> Array[String]

    The paths in this set, sorted, so a listing or a test is stable.

    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 a log collector (ELK, Loki) expects.

    RequestLog::fields

    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

    RsaDecrypter

    pub(all) struct RsaDecrypter {
    key :
    PrivateKey

    }

    A content-security key: an RSA private key and the PKCS#1 v1.5 unpad the envelope needs.

    RsaDecrypter::decrypt

    fn RsaDecrypter::decrypt(self : RsaDecrypter, block : Bytes) -> Bytes raise

    RsaDecrypter::from_der

    fn RsaDecrypter::from_der(der : BytesView) -> RsaDecrypter raise

    Build a decrypter from a PKCS#1 RSAPrivateKey DER structure, the body of a -----BEGIN RSA PRIVATE KEY----- block.

    RsaDecrypter::from_pem

    fn RsaDecrypter::from_pem(pem : StringView) -> RsaDecrypter raise

    Build a decrypter from a PEM document: a RSA PRIVATE KEY or a PRIVATE KEY (PKCS#8) block. The reader is a pure function of the text, so which file to read and how stays the caller's decision.

    RsaDecrypter::new

    Wrap an RSA private key as a decrypter.

    ServerMetrics

    The request metrics an HTTP service exposes: http_server_requests_duration_ms and http_server_requests_code_total, each partitioned by path, method and code. Held by the caller so it can be scraped after serving, and shared with RestEngine so the prometheus layer and the /metrics route read one set.

    ServerMetrics::latency

    The request-duration family, in milliseconds, labelled path, method, code.

    ServerMetrics::latency_total

    fn ServerMetrics::latency_total(self : ServerMetrics, path : String, verb : String, code : Int) -> Int64

    The latency observations recorded for one route and status.

    ServerMetrics::new

    A fresh metric set on its own registry — the two families ready, nothing observed yet. Raises Refused only if a metric or label name is invalid, which cannot happen for the fixed names here.

    ServerMetrics::observe

    fn ServerMetrics::observe(self : ServerMetrics, path : String, verb : String, code : Int, duration_ms : Double) -> Unit

    Record one finished request: count it under its path/method/code and observe duration_ms. A label set whose arity does not match the registered names is dropped rather than raised — both families always carry three labels, so this is unreachable from observe_request.

    ServerMetrics::registry

    The registry the families hang off, so a caller can add collectors of its own and publish everything through one scrape.

    ServerMetrics::request_count

    fn ServerMetrics::request_count(self : ServerMetrics, path : String, verb : String, code : Int) -> Double

    The counter value recorded for one route and status, 0 before the first request. A read-out for tests and for a health endpoint that reports its own traffic without scraping.

    ServerMetrics::requests

    The request counter family, labelled path, method, code.

    ServerMetrics::to_exposition

    fn ServerMetrics::to_exposition(self : ServerMetrics) -> String

    Render this set as the Prometheus text exposition format — the body metrics_handler serves.

    SignatureInput

    pub(all) struct SignatureInput {
    verb : String
    path : String
    query : String
    header : String?
    body : Bytes
    signed_uri : String?
    }

    A request as the signature check sees it.

    SignatureKey

    pub(all) struct SignatureKey {
    fingerprint : String
    decrypter : &Decrypter
    }

    One verification key: the fingerprint a caller names in the header, and the decrypter that opens envelopes sealed for it.

    SignatureVerifier

    pub(all) struct SignatureVerifier {
    keys : Array[SignatureKey]
    tolerance_ms : Int64
    strict : Bool
    }

    The verification policy: the keys, how far a timestamp may drift, and whether a request that fails verification is refused or merely ignored.

    SignatureVerifier::new

    fn SignatureVerifier::new(keys~ : Array[SignatureKey], tolerance_ms? : Int64, strict? : Bool) -> SignatureVerifier

    A verifier over keys. tolerance_ms defaults to one hour, the expiry go-zero's SignatureConf carries. A strict verifier refuses a request that fails; a lenient one serves it unsigned, which is how a service rolls the check out before every client signs.

    SignatureVerifier::strict

    fn SignatureVerifier::strict(self : SignatureVerifier) -> Bool

    Whether this verifier refuses a request it cannot verify.

    VerifiedContent

    pub(all) struct VerifiedContent {
    key : Bytes
    encrypted : Bool
    body : Bytes
    }

    The plain content a verified request carries: the HMAC key the envelope named, whether the body was sealed, and the body itself — already decrypted when it was.

    as_layer

    This middleware as a mid layer, for a stack built by @mid.stack.

    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.

    buffered_body

    fn buffered_body(body : Bytes) -> (async () ->
    Event
    )

    A receive channel that hands the application body once and then reports the connection closed — the shape an ASGI application sees for a body a layer has already buffered.

    constant_time_eq

    fn constant_time_eq(a : BytesView, b : BytesView) -> Bool

    Compare two byte strings without leaking where they differ: a MAC check that returns early lets an attacker count matched octets.

    content_security

    Content-security middleware: verify the X-Content-Security signature on DELETE, GET, POST and PUT requests, the methods the reference implementation checks.

    A strict verifier answers 403 Forbidden when the check fails. A lenient one serves the request anyway, which is what lets a fleet roll signing out one client at a time. When the envelope declares an encrypted payload, the body served to the application is the decrypted one.

    cors

    CORS middleware: answer a preflight OPTIONS with 204 No Content and the policy headers, and stamp the same headers onto every other response.

    A request from an origin the policy does not allow is served without the Access-Control-* headers rather than refused — the browser is the one that enforces the policy, and a server-side refusal would break non-browser clients that never asked for CORS at all.

    cors_allows

    fn cors_allows(allows : Array[String], origin : String) -> Bool

    Whether origin is allowed by allows: an exact match, a subdomain of an allowed origin, or a wildcard. Matching is case-insensitive on both sides, as the origin comparison in the Fetch standard is.

    cors_expose

    let cors_expose : String

    The response headers go-zero exposes to script.

    cors_headers

    let cors_headers : String

    The request headers go-zero lets through a preflight.

    cors_methods

    let cors_methods : String

    The method set go-zero advertises.

    default_latency_buckets

    let default_latency_buckets : Array[Double]

    The latency buckets go-zero registers for a server route, in milliseconds.

    exposition_content_type

    let exposition_content_type : String

    The content type a /metrics scrape carries so a Prometheus server reads the body as the text exposition format.

    file_server

    The file-serving layer: a GET under prefix whose remaining path names a registered file is answered with it, and every other request reaches the application. A conditional request whose If-None-Match names the file's tag is answered 304 without the body.

    This is a middleware rather than a route so it composes with the assembled chain exactly as the reference implementation's does: the file response still carries the CORS, trace and access-log layers.

    gunzip

    Gunzip middleware: when the request declares Content-Encoding: gzip, buffer the body, inflate it, and hand the wrapped application one HttpRequest carrying the plain body. A body that will not inflate is answered 400 Bad Request without reaching the application.

    The application is handed a scope without Content-Encoding and without the compressed body's Content-Length: both describe bytes that no longer exist by the time it looks, and a handler that trusted either would misread the request.

    limit caps the decompressed size in bytes, the number a compression bomb inflates to; 0 removes the cap, which is what a service that has already bounded the compressed body at the edge wants. Anything but an HTTP scope, a request without the header, or one with no body at all passes through untouched.

    headers_json

    fn headers_json(headers : Array[(String, String)]) ->
    Json

    A header list as a JSON object, so a verbose log line carries the whole exchange and not just the fields the record names.

    is_gzip_encoding

    fn is_gzip_encoding(value : String) -> Bool

    Whether a Content-Encoding value asks for gzip. The header is a comma separated list and gzip may be one of several codings, so the match is on the token, not the whole value.

    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
    Refused

    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.

    layer

    Adapt a moonasgi mid.Layer into a moonzero Middleware. The two shapes are the same ASGI transformer, so this is the whole bridge — a layer written for moonasgi (cors, gzip, body_limit, server_errors, trusted_hosts, sessions, or a user's own) drops into a moonzero chain unchanged.

    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.

    media_type_of

    fn media_type_of(path : String) -> String

    The media type for a path, from its extension. The table covers the types a static bundle actually carries; anything else is served as application/octet-stream, which every client downloads rather than guesses.

    metrics

    Metrics middleware: time each HTTP request on the shared clock and, when the response starts, count it under its path, method and status and record its latency in milliseconds. The record is taken once per request even if a downstream 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.

    mount_metrics

    fn mount_metrics(app :
    App
    , m : ServerMetrics) -> Unit

    Register the GET /metrics scrape endpoint on app, so a Prometheus server can pull the exposition.

    of_layer

    A mid layer as a middleware here, so a layer that package ships can sit in this service's chain.

    parse_i64

    fn parse_i64(text : String) -> Int64?

    Parse a decimal integer, optionally signed. A value that is empty, carries a non-digit, or overflows is None — the caller reports it as a malformed field rather than reading a truncated number.

    parse_pairs

    fn parse_pairs(value : String) -> Map[String, String]

    Split key=value pairs out of a header value: fields separated by ,, name and value by the first =, because a base64 value carries padding =. Both sides of a field are trimmed, and a field with no name is skipped.

    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.

    read_body

    async fn read_body(receive : async () ->
    Event
    ) -> Bytes

    Drain a receive channel into one body. Used by the layers that have to read the whole payload before they can decide anything.

    recovery

    Recovery middleware: run the wrapped application inside a try, and if it raises before any part of the response has gone out, emit a 500 InternalServer Error instead of letting the failure escape to the server.

    Once the response has started, a 500 can no longer be written — the status line is already on the wire — so the error is re-raised for the server to close the connection on. Answering late would append a second status to a response the client is already reading.

    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: 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.

    scope_header

    fn scope_header(scope :
    Scope
    , name : String) -> String?

    Read a header from an HTTP scope (lowercased-name convention); None for non-HTTP scopes or a missing header.

    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.

    An admitted request whose app answers 503 is settled as failed, any other as passed, which is the one status go-zero tells its shedder apart.

    sign_content

    fn sign_content(timestamp : String, verb : String, path : String, query : String, body : Bytes) -> Bytes

    The MAC preimage: timestamp, method, path, query and the hex SHA-256 of the body, newline separated.

    structured_logging

    Structured-logging middleware: 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.

    verbose adds both header sets and a preview of the request body to the entry — what a debugging deployment switches on and an ordinary one leaves off. The preview is the first verbose_body_preview bytes: go-zero dumps the whole body, which for a service that accepts uploads means a log line the size of the upload. 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): start a moonward Deadline of budget_ms at clock.now() for each request to 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).

    trace_id_of

    fn trace_id_of(traceparent : String) -> String?

    The trace id carried by a W3C traceparent value, 00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>, or None when the value is not one.

    tracing

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

    Tracing middleware: join the trace the request arrived on, or start one, and open a server span that reaches sink when the request finishes. Paths in ignore_paths are left untraced. The span and the propagation headers come from moontel's ASGI layer; this adds the convenience x-trace-id response header on top.

    The defaults — a discarding sink, always sampling, ids from the platform entropy source — make tracing() a working layer with nothing configured; a service that wants spans kept hands in a tracer over its own sink.

    unavailable

    let unavailable : Refusal

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

    verbose_body_preview

    let verbose_body_preview : Int

    How much of a request body a verbose access entry carries. Named because it is a policy, not an arithmetic detail: enough to recognise a payload, short of a log line the size of an upload.

    verify_signature

    fn verify_signature(verifier : SignatureVerifier, input : SignatureInput, now_ms : Int64) -> VerifiedContent raise Refused

    Verify one request against verifier at now_ms.

    On success the returned VerifiedContent carries the body to serve — the plaintext when the request declared an encrypted payload. A failure is the single check that refused it; the caller decides whether that is a 403 or a pass-through, per strict.