#Core

    The foundational HTTP types for Crescent: requests, responses, status codes, methods, and the Responder trait that ties them together.

    This package is a pure data layer -- it defines the types that flow through the framework but performs no I/O, no routing, and no async work. That makes it safe to import from any context: server handlers, middleware, fetch clients, test assertions, or standalone scripts that construct HTTP values for serialization.

    The main bobzhang/crescent package re-exports everything in this package, so application code can use either @crescent.HttpResponse or @core.HttpResponse interchangeably. Sub-packages that want a lighter dependency (no App, no server runtime) can import bobzhang/crescent/core directly.

    This package provides:

    • HttpRequest -- an incoming HTTP request (method, URL, headers, body) with helpers for path extraction, query parsing, cookie reading, and typed body deserialization.
    • HttpResponse -- an outgoing HTTP response (status code, headers, cookies, body) with a fluent builder API and factory methods for common status codes.
    • StatusCode -- a comprehensive enum covering every IANA-registered HTTP status code, plus a Custom(Int) escape hatch.
    • HttpMethod -- an enum for standard HTTP methods (GET, POST, PUT, etc.) with string round-tripping.
    • Responder -- a trait that any type can implement to become a valid response body. Built-in implementations for String, Json, Bytes, HttpResponse, HttpRequest, and Html.
    • BodyReader -- a trait for deserializing request bodies into typed values.

    #Install

    This package is included with bobzhang/crescent. To use it directly for a lighter dependency:

    import { "bobzhang/crescent/core" ... }

    #HttpRequest

    An HttpRequest bundles the HTTP method, URL, headers, and raw body bytes. The framework constructs these from incoming TCP connections, but you can also create them directly for testing or for building outgoing requests with @fetch.request.

    ///|
    test "construct and inspect a request" {
    let req = @core.HttpRequest(
    Get,
    "/users?page=2",
    { "Authorization": "Bearer token" },
    raw_body=b"",
    )
    debug_inspect(
    req,
    content=(
    #|{
    #| http_method: Get,
    #| url: "/users?page=2",
    #| headers: { "Authorization": "Bearer token" },
    #| raw_body: ...,
    #| cached_path: None,
    #| cached_query_params: None,
    #|}
    ),
    )
    assert_eq(req.path(), "/users")
    assert_eq(req.query_string(), Some("page=2"))
    assert_eq(req.get_query("page"), Some("2"))
    assert_eq(req.get_header("authorization"), Some("Bearer token"))
    }

    #Path and query parsing

    path() extracts the URL path, stripping the query string and fragment. query_string() returns the raw query portion. get_query(key) does a decoded key lookup. All three cache their results so repeated calls are free.

    ///|
    test "query parameters are percent-decoded" {
    let req = @core.HttpRequest(
    Get,
    "/search?q=hello%20world&lang=en",
    {},
    raw_body=b"",
    )
    |> x => { (x.get_query("q"), x.get_query("lang"), x.get_query("missing")) }
    debug_inspect(
    req,
    content=(
    #|(Some("hello world"), Some("en"), None)
    ),
    )
    }

    #Typed body reading

    req.body[T]() deserializes the raw bytes into any type that implements the BodyReader trait. Built-in readers exist for String, Json, Bytes, FixedArray[Byte], and Array[Byte]:

    ///|
    test "read body as string" {
    let req = @core.HttpRequest(Post, "/", {}, raw_body=b"hello")
    let text : String = req.body()
    assert_eq(text, "hello")
    }

    ///|
    test "read body as JSON" {
    let req = @core.HttpRequest(Post, "/", {}, raw_body=b"{\"name\":\"Alice\"}")
    let body : Json = req.body()
    debug_inspect(
    body,
    content=(
    #|Object({ "name": String("Alice") })
    ),
    )
    }

    #JSON deserialization shorthand

    req.json[T]() parses the body as JSON and deserializes into any FromJson type in one step:

    ///|
    #warnings("-unnecessary_annotation")
    struct CoreDocUser {
    name : String
    age : Int
    } derive(FromJson, Eq, Debug)

    ///|
    test "json shorthand parses typed value" {
    let req = @core.HttpRequest(
    Post,
    "/",
    {},
    raw_body=b"{\"name\":\"Bob\",\"age\":30}",
    )
    let user : CoreDocUser = req.json()
    debug_inspect(
    user,
    content=(
    #|{ name: "Bob", age: 30 }
    ),
    )
    }

    get_cookie(name) parses the Cookie header (case-insensitive lookup) and returns the matching CookieItem, or None if absent:

    ///|
    test "read a cookie from the request" {
    let req = @core.HttpRequest(
    Get,
    "/",
    { "Cookie": "session=abc123; theme=dark" },
    raw_body=b"",
    )
    guard req.get_cookie("session") is Some(c) else {
    fail("expected session cookie")
    }
    assert_eq(c.value, "abc123")
    assert_eq(req.get_cookie("missing"), None)
    }

    #HttpResponse

    An HttpResponse carries a status code, headers, cookies, and a body. Build one with the constructor or the named factory methods, then chain .body(), .header(), .json(), or .json_value() to finalize it.

    #Factory methods

    ///|
    test "factory methods set the right status code" {
    assert_eq(@core.HttpResponse::ok().status_code, OK)
    assert_eq(@core.HttpResponse::created().status_code, Created)
    assert_eq(@core.HttpResponse::not_found().status_code, NotFound)
    assert_eq(@core.HttpResponse::bad_request().status_code, BadRequest)
    assert_eq(
    @core.HttpResponse::internal_server_error().status_code,
    InternalServerError,
    )
    }

    #Fluent builder

    ///|
    test "fluent response building" {
    let res = @core.HttpResponse::ok()
    .header("X-Request-Id", "abc-123")
    .body(@core.html("<h1>Hello</h1>"))
    assert_eq(res.status_code, OK)
    debug_inspect(
    res.headers.get("Content-Type"),
    content="Some(\"text/html; charset=utf-8\")",
    )
    debug_inspect(res.headers.get("X-Request-Id"), content="Some(\"abc-123\")")
    }

    #JSON responses

    json() takes any &ToJson and json_value() takes a concrete T : ToJson. Both set Content-Type: application/json; charset=utf-8:

    ///|
    test "json response sets content type and body" {
    let res = @core.HttpResponse::ok().json(({ "status": "ok" } : Json))
    debug_inspect(
    res.headers.get("Content-Type"),
    content="Some(\"application/json; charset=utf-8\")",
    )
    }

    #Redirects

    ///|
    test "redirect helpers" {
    let r301 = @core.HttpResponse::redirect("/new")
    debug_inspect(
    r301,
    content=(
    #|{
    #| status_code: MovedPermanently,
    #| headers: { "Location": "/new" },
    #| cookies: {},
    #| raw_body: ...,
    #|}
    ),
    )

    let r302 = @core.HttpResponse::redirect_temporary("/temp")
    assert_eq(r302.status_code, Found)

    let r307 = @core.HttpResponse::redirect_307("/keep-method")
    assert_eq(r307.status_code, TemporaryRedirect)
    }

    #Cookies

    set_cookie and delete_cookie manage response cookies with full attribute support. The framework serializes them into Set-Cookie headers when sending the response:

    ///|
    test "set and delete cookies" {
    let res = @core.HttpResponse::ok()
    res.set_cookie("session", "xyz", path="/", http_only=true, secure=true)
    guard res.cookies.get("session") is Some(c) else {
    fail("expected session cookie")
    }
    assert_eq(c.value, "xyz")
    assert_eq(c.path, Some("/"))
    assert_eq(c.http_only, Some(true))

    // Deleting sets Max-Age=0
    res.delete_cookie("session")
    guard res.cookies.get("session") is Some(deleted) else {
    fail("expected deleted cookie")
    }
    assert_eq(deleted.max_age, Some(0))
    }

    #Error responses

    HttpResponse::error(status, message) creates a JSON error body with the status code and message, suitable for API error responses:

    ///|
    test "error helper creates structured JSON body" {
    let res = @core.HttpResponse::error(BadRequest, "name is required")
    assert_eq(res.status_code, BadRequest)
    let body : String = res.read_body()
    assert_true(body.contains("name is required"))
    assert_true(body.contains("400"))
    }

    #StatusCode

    A comprehensive enum covering every IANA-registered status code. Pattern match on it directly -- no need to compare raw integers:

    ///|
    test "status code round-trips through integer" {
    assert_eq(@core.StatusCode::from_int(200), OK)
    assert_eq(@core.StatusCode::from_int(404), NotFound)
    let ok : @core.StatusCode = OK
    assert_eq(ok.to_int(), 200)
    // Unknown codes become Custom
    assert_eq(@core.StatusCode::from_int(599), Custom(599))
    }

    #HttpMethod

    ///|
    test "method round-trips through string" {
    assert_eq(@core.HttpMethod::from_string("GET"), Get)
    let get : @core.HttpMethod = Get
    assert_eq(get.to_method_string(), "GET")
    // Unknown methods use Other
    let custom = @core.HttpMethod::from_string("PURGE")
    assert_eq(custom.to_method_string(), "PURGE")
    }

    #Traits

    This package defines two open traits that drive polymorphic request and response handling: Responder (any value → HTTP response body) and BodyReader (request/response bytes → typed value). Both are pub(open), so user code is free to add new implementations.

    #trait Responder

    ///|
    pub(open) trait Responder {
    fn options(Self, HttpResponse) -> Unit
    fn output(Self, Buffer) -> Unit
    fn output_bytes(Self) -> Bytes?
    }

    The Responder trait is the adapter between any MoonBit value and an HTTP response body. Handlers return &Responder, and the framework calls three methods on it:

    MethodPurpose
    options(res)Set status code and headers (e.g. Content-Type) on the response
    output(buf)Write the body bytes into a buffer
    output_bytes()Return pre-encoded bytes directly (fast path, avoids a copy)

    Built-in implementations:

    TypeContent-Type
    String / StringViewtext/plain; charset=utf-8
    Json / &ToJsonapplication/json; charset=utf-8
    Bytesapplication/octet-stream
    HttpResponseCopies status, merges headers, forwards body
    HttpRequestMerges headers, forwards body (useful for proxying)
    Html (via html())text/html; charset=utf-8

    #String as a responder

    The simplest handler just returns a string -- Crescent wraps it in the Responder impl that sets text/plain:

    ///|
    test "string responder sets text/plain" {
    let res = @core.HttpResponse(status_code=OK)
    let responder : &@core.Responder = "hello"
    responder.options(res)
    debug_inspect(
    res.headers.get("Content-Type"),
    content="Some(\"text/plain; charset=utf-8\")",
    )
    }

    #html() and text() helpers

    html() creates an Html responder that sets text/html; charset=utf-8. text() creates a plain-text responder. Both accept any &Show value:

    ///|
    test "html helper sets text/html content type" {
    let res = @core.HttpResponse(status_code=OK)
    let responder = @core.html("<h1>Hello</h1>")
    responder.options(res)
    debug_inspect(
    res.headers.get("Content-Type"),
    content="Some(\"text/html; charset=utf-8\")",
    )
    let buf = Buffer()
    responder.output(buf)
    assert_eq(buf.contents(), @utf8.encode("<h1>Hello</h1>"))
    }

    #trait BodyReader

    ///|
    pub(open) trait BodyReader {
    fn from_request(HttpRequest) -> Self raise
    }

    The BodyReader trait powers typed body deserialization for both requests and responses. Implement it for your own types to enable req.body[MyType]() and res.read_body[MyType]():

    TypeBehavior
    StringUTF-8 decode (raises on invalid bytes)
    JsonParse as JSON value
    BytesReturn raw bytes as-is
    FixedArray[Byte]Convert to fixed-size byte array
    Array[Byte]Convert to resizable byte array

    ///|
    test "read response body as typed value" {
    let res = @core.HttpResponse(status_code=OK, raw_body=b"hello")
    let text : String = res.read_body()
    assert_eq(text, "hello")
    }

    BodyReader

    pub(open) trait BodyReader {
    fn from_request(req : HttpRequest) -> Self raise
    }

    Trait for types that can be deserialized from an HTTP request body.
    impl BodyReader for Bytes
    impl BodyReader for Json

    Responder

    pub(open) trait Responder {
    fn options(Self, res : HttpResponse) -> Unit
    fn output(Self, buf :
    Buffer
    ) -> Unit
    fn output_bytes(Self) -> Bytes?
    }

    Trait for types that can be sent as an HTTP response body.
    impl Responder for String
    impl Responder for Bytes
    impl Responder for Json
    impl Responder for ToJson

    Html

    type Html

    impl Responder for Html

    Html::options

    fn Html::options(_ : Html, res : HttpResponse) -> Unit

    Html::output

    fn Html::output(self : Html, buf :
    Buffer
    ) -> Unit

    Html::output_bytes

    fn Html::output_bytes(_self : Html) -> Bytes?

    HttpMethod

    pub(all) enum HttpMethod {
    Get
    Head
    Post
    Put
    Patch
    Delete
    Options
    Trace
    Connect
    Other(String)
    } derive(Eq,
    Debug
    )

    HTTP request methods as a type-safe enum.

    Using this enum instead of raw strings prevents typos like "DLETE" or "OPTONS" and enables pattern matching in handlers.
    impl Show for HttpMethod

    HttpMethod::equal

    fn HttpMethod::equal(HttpMethod, HttpMethod) -> Bool

    HttpMethod::from_string

    fn HttpMethod::from_string(s : String) -> HttpMethod

    Parses a string into an HttpMethod.

    HttpMethod::not_equal

    fn HttpMethod::not_equal(x : HttpMethod, y : HttpMethod) -> Bool

    HttpMethod::output

    fn HttpMethod::output(self : HttpMethod, logger : &Logger) -> Unit

    HttpMethod::to_method_string

    fn HttpMethod::to_method_string(self : HttpMethod) -> String

    Converts the enum to its HTTP method string representation.

    HttpMethod::to_string

    fn HttpMethod::to_string(self : HttpMethod) -> String

    HttpRequest

    pub(all) struct HttpRequest {
    http_method : HttpMethod
    url : String
    headers : Map[String, String]
    raw_body : Bytes
    // private fields
    } derive(
    Debug
    )

    An incoming HTTP request containing the method, URL, headers, and body.

    HttpRequest::HttpRequest

    fn HttpRequest::HttpRequest(http_method : HttpMethod, url : String, headers : Map[String, String], raw_body? : Bytes) -> HttpRequest

    Creates a new HttpRequest with the given method, URL, headers, and optional body.

    HttpRequest::body

    fn[T : BodyReader] HttpRequest::body(self : HttpRequest) -> T raise

    Deserializes the request body into a value of type T via the BodyReader trait.

    HttpRequest::content_type

    fn HttpRequest::content_type(self : HttpRequest) -> String?

    Returns the Content-Type header value, or None if not set.

    HttpRequest::from_method_string

    fn HttpRequest::from_method_string(http_method : String, url : String, headers : Map[String, String], raw_body : Bytes) -> HttpRequest

    Creates a new HttpRequest from a method string, parsing it into an HttpMethod.

    Retrieves a cookie by name from the request's Cookie header, if present.

    HttpRequest::get_header

    fn HttpRequest::get_header(self : HttpRequest, name : String) -> String?

    Returns the value of a request header by name (case-insensitive), or None if not found.

    HttpRequest::get_query

    fn HttpRequest::get_query(self : HttpRequest, key : String) -> String?

    Looks up a single query parameter by key, returning None if not found. Uses the cached query params — calling this multiple times with different keys only parses the URL once.

    HttpRequest::json

    Parses the request body as JSON and deserializes into type T.

    HttpRequest::method_string

    fn HttpRequest::method_string(self : HttpRequest) -> String

    Returns the HTTP method as a string (e.g., "GET", "POST").

    HttpRequest::options

    fn HttpRequest::options(self : HttpRequest, res : HttpResponse) -> Unit

    HttpRequest::output

    HttpRequest::output_bytes

    fn HttpRequest::output_bytes(self : HttpRequest) -> Bytes?

    HttpRequest::path

    fn HttpRequest::path(self : HttpRequest) -> String

    Returns the path component of the request URL, excluding query string and fragment.

    HttpRequest::query_params

    fn HttpRequest::query_params(self : HttpRequest) -> Map[String, String]

    Parses the query string into a map of key-value pairs.

    Returns a fresh copy on every call — mutating the returned map does NOT affect subsequent get_query() calls. The underlying parse is cached, so calling this multiple times only parses the URL once.

    HttpRequest::query_string

    fn HttpRequest::query_string(self : HttpRequest) -> String?

    Returns the raw query string from the request URL, or None if absent.

    HttpRequest::try_json

    fn[T :
    FromJson
    ] HttpRequest::try_json(self : HttpRequest) -> Result[T, String]

    Tries to parse the request body as JSON, returning Ok(T) on success or Err(message) on failure.

    HttpResponse

    pub(all) struct HttpResponse {
    status_code : StatusCode
    headers : Map[String, String]
    cookies : Map[String,
    CookieItem
    ]
    raw_body : Bytes
    } derive(
    Debug
    )

    An outgoing HTTP response containing the status code, headers, cookies, and body.

    HttpResponse::HttpResponse

    fn HttpResponse::HttpResponse(status_code~ : StatusCode, headers? : Map[String, String], cookies? : Map[String,
    CookieItem
    ], raw_body? : Bytes) -> HttpResponse

    Creates a new HttpResponse with the given status code and optional headers, cookies, and body.

    HttpResponse::bad_request

    fn HttpResponse::bad_request() -> HttpResponse

    Creates a 400 Bad Request response.

    HttpResponse::body

    fn HttpResponse::body(self : HttpResponse, body : &Responder) -> HttpResponse

    Sets the response body from any Responder and returns the response for chaining.

    Applies the responder's options() (e.g. sets Content-Type) and uses the output_bytes() fast path when available to avoid an intermediate buffer copy.

    HttpResponse::created

    fn HttpResponse::created() -> HttpResponse

    Creates a 201 Created response.
    fn HttpResponse::delete_cookie(self : HttpResponse, key : String) -> Unit

    Deletes a cookie by setting its value to empty and max-age to 0.

    HttpResponse::error

    fn HttpResponse::error(status : StatusCode, message : String) -> HttpResponse

    Creates an error response with a JSON body containing status and message.

    HttpResponse::forbidden

    fn HttpResponse::forbidden() -> HttpResponse

    Creates a 403 Forbidden response.

    HttpResponse::header

    fn HttpResponse::header(self : HttpResponse, name : String, value : String) -> HttpResponse

    Sets a response header and returns self for fluent chaining.

    Uses case-insensitive matching: setting Content-Type will replace any existing content-type or CONTENT-TYPE header.

    HttpResponse::internal_server_error

    fn HttpResponse::internal_server_error() -> HttpResponse

    Creates a 500 Internal Server Error response.

    HttpResponse::json

    fn HttpResponse::json(self : HttpResponse, obj : &ToJson) -> HttpResponse

    Sets the response body to the JSON representation of the given value. Also sets Content-Type: application/json; charset=utf-8.

    HttpResponse::json_value

    fn[T : ToJson] HttpResponse::json_value(self : HttpResponse, value : T) -> HttpResponse

    Serializes a typed value as JSON, sets Content-Type, and returns the response.

    Uses case-insensitive header matching: any existing content-type / CONTENT-TYPE header is replaced, not duplicated.

    HttpResponse::no_content

    fn HttpResponse::no_content() -> HttpResponse

    Creates a 204 No Content response.

    HttpResponse::not_found

    fn HttpResponse::not_found() -> HttpResponse

    Creates a 404 Not Found response.

    HttpResponse::ok

    Creates a 200 OK response.

    HttpResponse::options

    fn HttpResponse::options(self : HttpResponse, res : HttpResponse) -> Unit

    HttpResponse::output

    HttpResponse::output_bytes

    fn HttpResponse::output_bytes(self : HttpResponse) -> Bytes?

    HttpResponse::read_body

    fn[T : BodyReader] HttpResponse::read_body(self : HttpResponse) -> T raise

    Deserializes the response body into a value of type T via the BodyReader trait.

    HttpResponse::redirect

    fn HttpResponse::redirect(location : String) -> HttpResponse

    Returns a 301 Moved Permanently redirect response.

    HttpResponse::redirect_307

    fn HttpResponse::redirect_307(location : String) -> HttpResponse

    Returns a 307 Temporary Redirect response (preserves method).

    HttpResponse::redirect_308

    fn HttpResponse::redirect_308(location : String) -> HttpResponse

    Returns a 308 Permanent Redirect response (preserves method).

    HttpResponse::redirect_temporary

    fn HttpResponse::redirect_temporary(location : String) -> HttpResponse

    Returns a 302 Found (temporary) redirect response.
    fn HttpResponse::set_cookie(self : HttpResponse, name : String, value : String, max_age? : Int, path? : String, domain? : String, secure? : Bool, http_only? : Bool, same_site? :
    SameSiteOption
    ) -> Unit

    Sets a cookie on the response with the given name, value, and optional attributes.

    HttpResponse::to_responder

    fn HttpResponse::to_responder(self : HttpResponse) -> &Responder

    Converts this response into a Responder trait object for use as a handler return value.

    HttpResponse::unauthorized

    fn HttpResponse::unauthorized() -> HttpResponse

    Creates a 401 Unauthorized response.

    StatusCode

    pub(all) enum StatusCode {
    Continue
    SwitchingProtocols
    Processing
    EarlyHints
    OK
    Created
    Accepted
    NonAuthoritativeInfo
    NoContent
    ResetContent
    PartialContent
    MultiStatus
    AlreadyReported
    IMUsed
    MultipleChoices
    MovedPermanently
    Found
    SeeOther
    NotModified
    UseProxy
    TemporaryRedirect
    PermanentRedirect
    BadRequest
    Unauthorized
    PaymentRequired
    Forbidden
    NotFound
    MethodNotAllowed
    NotAcceptable
    ProxyAuthRequired
    RequestTimeout
    Conflict
    Gone
    LengthRequired
    PreconditionFailed
    RequestEntityTooLarge
    RequestUriTooLong
    UnsupportedMediaType
    RequestedRangeNotSatisfiable
    ExpectationFailed
    Teapot
    MisdirectedRequest
    UnprocessableEntity
    Locked
    FailedDependency
    TooEarly
    UpgradeRequired
    PreconditionRequired
    TooManyRequests
    RequestHeaderFieldsTooLarge
    UnavailableForLegalReasons
    InternalServerError
    NotImplemented
    BadGateway
    ServiceUnavailable
    GatewayTimeout
    HttpVersionNotSupported
    VariantAlsoNegotiates
    InsufficientStorage
    LoopDetected
    NotExtended
    NetworkAuthenticationRequired
    Custom(Int)
    } derive(Eq, ToJson,
    Debug
    )

    HTTP status codes as registered with IANA. See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
    impl Show for StatusCode

    StatusCode::equal

    fn StatusCode::equal(StatusCode, StatusCode) -> Bool

    StatusCode::from_int

    fn StatusCode::from_int(i : Int) -> StatusCode

    Converts an integer HTTP status code to a StatusCode enum value.

    StatusCode::not_equal

    fn StatusCode::not_equal(x : StatusCode, y : StatusCode) -> Bool

    StatusCode::output

    fn StatusCode::output(self : StatusCode, logger : &Logger) -> Unit

    StatusCode::to_int

    fn StatusCode::to_int(self : StatusCode) -> Int

    Returns the integer value of this HTTP status code.

    StatusCode::to_json

    fn StatusCode::to_json(StatusCode) -> Json

    StatusCode::to_string

    fn StatusCode::to_string(self : StatusCode) -> String

    html

    fn html(html : &Show) -> &Responder

    Creates an HTML responder from any Show value, setting the Content-Type to text/html.

    text

    fn text(text : &Show) -> &Responder

    Creates a plain-text responder from any Show value, setting the Content-Type to text/plain.