Skip to content

AbrJA/Mongoose.jl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

281 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mongoose.jl

Production-ready HTTP & WebSocket framework for Julia
Built on the battle-tested Mongoose C library

Documentation Build Status Julia 1.10+ License


Why Mongoose.jl? 🚀

Category Highlights
Performance Sub-100ms TTFR via precompilation. Zero-allocation static router. C-level static file serving with Range, ETag, gzip.
Architecture Sync (Server) and async (Async) modes. Multi-worker pool with backpressure. Per-request timeouts with thread starvation protection.
Routing Trie-based O(1) matching. Typed path parameters (:id::Int). Wildcards (*path). Automatic HEAD from GET handlers.
WebSocket Same port as HTTP. Frame size limits. Idle timeout. Upgrade rejection. Ping/pong (RFC 6455).
Middleware CORS, rate limiting, bearer/API key auth, structured logging, Prometheus metrics, health checks. Path-scoped via paths=.
Production Graceful shutdown with drain. 503 backpressure on overload. Custom error responses. X-Request-Id propagation.
AOT Full juliac --trim=safe support via @router macro. Zero startup time compiled binaries.
Minimal Only Mongoose_jll + PrecompileTools. No HTTP.jl, no Sockets.jl. JSON via optional one-line extension (see JSON).

Installation 📦

] add Mongoose

For JSON support:

] add JSON

Quick Start ⚡

using Mongoose

router = Router()

route!(router, :get, "/", req -> Response(Plain, "Hello from Mongoose.jl!"))

route!(router, :get, "/users/:id::Int", (req, id) ->
    Response(Json, """{"id": $id, "name": "User $id"}""")
)

route!(router, :post, "/echo", req -> Response(Plain, req.body))

server = Async(router; nworkers=4)
start!(server, port=8080, blocking=false)

# Graceful shutdown when done
shutdown!(server)

Routing 🧭

HTTP Methods 🌐

route!(router, :get,    "/items",     req -> ...)
route!(router, :post,   "/items",     req -> ...)
route!(router, :put,    "/items/:id", (req, id) -> ...)
route!(router, :patch,  "/items/:id", (req, id) -> ...)
route!(router, :delete, "/items/:id", (req, id) -> ...)

GET routes automatically handle HEAD requests (body stripped, headers preserved).

Typed Path Parameters 🔢

Append ::Type to a segment for automatic parsing. Invalid values return 404 (e.g. /users/abc when Int is expected):

route!(router, :get, "/users/:id::Int",       (req, id)   -> ...)  # id::Int
route!(router, :get, "/price/:val::Float64",  (req, val)  -> ...)  # val::Float64
route!(router, :get, "/posts/:slug",          (req, slug) -> ...)  # slug::String

Query String 🔍

Access query parameters via req.query, which is a Dict{String,String}:

route!(router, :get, "/search", req -> begin
    q     = get(req.query, "q", "")
    page  = something(tryparse(Int, get(req.query, "page", "1")), 1)
    limit = tryparse(Int, get(req.query, "limit", ""))
    Response(Plain, "Searching: $q, page $page")
end)

Request Helpers 🛠️

Expression Returns Description
req.body String Raw request body
get(req.headers, "authorization", nothing) String | nothing Case-insensitive header lookup
req.query Dict{String,String} Parsed query map (e.g. Dict("q" => "test", "page" => "2"))
context!(req) Dict{Symbol,Any} Lazily-allocated context dict (set by middleware)

Server Types 🖥️

Type Model Best for
Async Event loop + N worker tasks via channels Production APIs
Server Blocking event loop on caller's thread Scripts, AOT binaries
# Production: 4 workers, 5s per-request timeout, 4 MB body limit, 60s WS idle timeout
server = Async(router;
    nworkers        = 4,
    nqueue          = 1024,
    request_timeout = 5000,
    max_body        = 4 * 1024 * 1024,
    ws_idle_timeout = 60,
)

# AOT / simple scripts
server = Server(router)

Config ⚙️

Consolidate all options into a Config struct — particularly useful for environment-driven configuration:

config = Config(
    nworkers        = parse(Int, get(ENV, "NWORKERS", "4")),
    max_body        = parse(Int, get(ENV, "MAX_BODY", "1048576")),
    request_timeout = parse(Int, get(ENV, "REQ_TIMEOUT", "0")),
    ws_idle_timeout = 60,
    drain_timeout   = 10_000,
)

server = Async(router, config)   # or Server(router, config)

Custom Error Responses 🧯

Register pre-built responses for specific status codes — no function callbacks, fully trim-safe:

fail!(server, 500, Response(Json, """{"error":"Internal error"}"""; status=500))
fail!(server, 413, Response(Json, """{"error":"Body too large"}"""; status=413))
fail!(server, 503, Response(Json, """{"error":"Service overloaded"}"""; status=503))
fail!(server, 504, Response(Json, """{"error":"Timed out"}"""; status=504))

# Custom 404 — add a catch-all route
route!(router, :get, "*", req -> Response(Html, read("404.html", String); status=404))

Responses 📬

Response(Plain, "Hello!")                    # text/plain, status 200
Response(Json, """{"ok": true}""")          # application/json, status 200
Response(Html, "<p>ok</p>")                 # text/html, status 200
Response(Json, body; status=201)            # custom status
Response(Html, body; status=404)            # custom status
Response(Json, body; headers=["X-Custom" => "value"])  # extra headers

The format type sets the Content-Type header automatically:

Format Content-Type
Plain text/plain; charset=utf-8
Html text/html; charset=utf-8
Json application/json; charset=utf-8
Xml application/xml; charset=utf-8
Css text/css; charset=utf-8
Js application/javascript; charset=utf-8
Binary application/octet-stream

Middleware 🧩

Middleware runs in registration order. Each middleware can inspect and modify the request, short-circuit with a response, or pass through to the next handler.

server = Async(router)

# Structured JSON access logs
plug!(server, logger(structured=true))

# CORS — allow a specific origin
plug!(server, cors(origins="https://myapp.com"))

# Rate limiting — 200 requests per 60s per client IP
plug!(server, ratelimit(max_requests=200, window_seconds=60))

# Auth — scoped to /api routes only
plug!(server, bearer(token -> token == "my-secret"); paths=["/api"])

# API key auth
plug!(server, apikey(header_name="X-API-Key", keys=Set(["key-abc", "key-xyz"])))

# Serve static files from the "public/" directory (C-level, with Range/ETag/gzip)
mount!(server, "public")

# Prometheus-compatible metrics at GET /metrics
plug!(server, metrics())

# Built-in health check at GET /healthz
plug!(server, health())

Path-Scoped Middleware 🎯

The paths keyword limits a middleware to specific URL prefixes only:

plug!(server, bearer(t -> t == "secret"); paths=["/api", "/admin"])
plug!(server, ratelimit(max_requests=10);       paths=["/api/expensive"])

Custom Middleware 🧪

Subtype AbstractMiddleware and implement the call operator:

struct RequestTimer <: Mongoose.AbstractMiddleware end

function (::RequestTimer)(req::Request, params, next)
    t = time()
    res = next()
    elapsed = round((time() - t) * 1000, digits=1)
    @info "$(req.method) $(req.uri)" status=res.status ms=elapsed
    return res
end

plug!(server, RequestTimer())

WebSocket Support 🔌

ws!(router, "/chat",
    on_message = (msg::Message) -> Message("Echo: $(msg.data)"),
    on_open    = (req::Request) -> begin
        # Return false to reject the upgrade (sends 403)
        auth = get(req.headers, "authorization", nothing)
        auth === nothing && return false
        @info "WS connected" uri=req.uri
    end,
    on_close   = () -> @info "WS disconnected"
)
Callback Signature Notes
on_open (req::Request) → Any Return false to reject upgrade with 403. Optional.
on_message (msg::Message) → Message | String | Vector{UInt8} | nothing Called per frame. Return nothing for no reply.
on_close () → Any No arguments — connection is already gone. Optional.

Production features:

  • Frame size limit — enforced via max_body (same limit as HTTP). Oversized frames close the connection.
  • Idle timeout — set ws_idle_timeout (seconds) to auto-close stale connections.
  • Upgrade rejection — return false from on_open to refuse the connection with 403.
  • Ping/pong — automatic RFC 6455 control frame handling.

💡 Tip: start with ws_idle_timeout=60 and tune based on your client behavior.


JSON 🧾

Install JSON.jl and extend encode once at startup:

using Mongoose, JSON

Mongoose.encode(::Type{Json}, body) = JSON.json(body)

Then use Response(Json, value) anywhere — Content-Type is set automatically:

route!(router, :get, "/users/:id::Int", (req, id) ->
    Response(Json, Dict("id" => id, "name" => "User $id"))
)

route!(router, :post, "/users", req -> begin
    data = JSON.parse(req.body)
    Response(Json, Dict("created" => get(data, "name", "")); status=201)
end)

AOT Compilation with juliac 🧊

Use the @router macro to generate a zero-allocation, compile-time dispatch function — required for juliac --trim=safe. No dynamic dispatch, no closures, no allocations on the hot path.

# app.jl
using Mongoose

@router MyApp begin
    get("/", req -> Response(Plain, "Hello!"))
    get("/users/:id::Int", (req, id) -> Response(Plain, "User $id"))
    post("/echo", req -> Response(Plain, req.body))
    ws("/live", on_message = msg -> Message("Echo: $(msg.data)"))
end

(@main)(ARGS) = begin
    server = Server(MyApp)
    start!(server, port=8080, blocking=true)
    return 0
end

Compile and run:

juliac --trim=safe --project . --output-exe myapp app.jl
./myapp   # starts instantly — no JIT warmup

Logging 📋

By default, Mongoose.jl routes @log_info, @log_warn, and @log_error macros to Julia's native @info, @warn, @error — integrating seamlessly with ConsoleLogger, TeeLogger, and log filtering.

For juliac --trim=safe binaries, set LOG_TRIMMABLE=true to force concrete print()-based logging instead (recommended because Base/CoreLogging dispatch can be trimmed or unreliable in --trim=safe builds):

# Native logging (default, works in JIT and AOT binary)
julia --project app.jl

# For trim-safe binaries, use concrete logging
LOG_TRIMMABLE=true juliac --trim=safe --project . --output-exe binary app.jl
./binary                              # @log_* routes to concrete print() logging

Full Example 🏗️

A complete app with REST API, WebSocket, middleware stack, and custom errors:

using Mongoose, JSON

Mongoose.encode(::Type{Json}, body) = JSON.json(body)

router = Router()

# Health check
route!(router, :get, "/health", req -> Response(Plain, "ok"))

# REST API
route!(router, :get, "/api/users/:id::Int", (req, id) ->
    Response(Json, Dict("id" => id, "name" => "User $id"))
)

route!(router, :post, "/api/users", req -> begin
    data = JSON.parse(req.body)
    Response(Json, Dict("created" => get(data, "name", "")); status=201)
end)

# WebSocket
ws!(router, "/ws",
    on_message = (msg::Message) -> Message("""{"ack":true}"""),
    on_open = (req::Request) -> @info "WS connected" uri=req.uri,
    on_close = () -> @info "WS disconnected"
)

# Custom 404
route!(router, :get, "*", req -> Response(Html, "<h1>Not Found</h1>"; status=404))

# Server
server = Async(router; nworkers=4, request_timeout=10_000, ws_idle_timeout=60)

plug!(server, logger(structured=true))
plug!(server, cors(origins="https://myapp.com"))
plug!(server, ratelimit(max_requests=30, window_seconds=60))
plug!(server, bearer(t -> t == get(ENV, "API_TOKEN", "secret")); paths=["/api"])
mount!(server, "public")

fail!(server, 500, Response(Json, Dict("error" => "Internal error"); status=500))

# blocking=true: start! handles Ctrl+C automatically and shuts down gracefully
start!(server, port=8080)

Deployment 🌐

Mongoose.jl serves plain HTTP. For HTTPS in production, terminate TLS at a reverse proxy in front of your Mongoose.jl process.

Proxy Notes
Caddy Use automatic certificate management and proxy to 127.0.0.1:8080.
nginx Terminate TLS at the edge and proxy_pass to http://127.0.0.1:8080.
Envoy Terminate TLS at the listener and route to the Mongoose.jl upstream cluster.

This deployment model keeps certificate rotation, HTTP/2 negotiation, and edge policy out of application code.


Documentation 📚

Full API reference and examples: AbrJA.github.io/Mongoose.jl

License ⚖️

Distributed under the GPL-2 License. See LICENSE for details.

About

Julia package to build simple web servers

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages