8000 GitHub - AbrJA/Mongoose.jl: Julia package to build simple web servers Β· GitHub
[go: up one dir, main page]

Skip to content

About

Julia package to build simple web servers

Resources

Stars

16 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

462 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Mongoose.jl

Production-ready HTTP & WebSocket framework for Julia
Powered by the battle-tested Mongoose C library

Documentation Build Status Julia 1.10+ License


✨ Why Mongoose.jl?

  • ⚑ Fast β€” precompiled cold start; freeze! compiles the route table so warm dispatch stays at ~100 ns (fixed routes) / ~400 ns (typed params) with minimal allocation.
  • πŸ§ͺ Testable without FFI β€” FakeTransport drives the full pipeline in pure Julia: no ports, no C library, deterministic tests.
  • 🧩 Batteries included β€” CORS, rate limiting, bearer/API-key/basic auth, gzip, ETag, security headers, structured logs, Prometheus metrics, health checks, static files.
  • πŸ“‘ Real-time built in β€” WebSocket (including server-initiated push) and Server-Sent Events on the same port, with backpressure and graceful drain.
  • πŸ”Œ Replaceable parts β€” router, executor, and transport sit behind small protocols; the core has no FFI dependency and can be swapped or embedded.
  • πŸ›‘οΈ Production defaults β€” graceful shutdown on SIGINT and SIGTERM, request timeouts, header timeouts, connection caps, 503 backpressure, typed error handlers, dependency injection.

πŸš€ Quick Start

using Pkg; Pkg.add("Mongoose")
using Mongoose

app = App(workers=4)

get!(app, "/") do req
    text("Hello from Mongoose.jl!")
end

get!(app, "/users/:id::Int") do req, id
    json(Dict("id" => id, "name" => "User $id"))
end

post!(app, "/echo") do req
    json(parsejson(req); status=201)
end

start!(app; port=8080)

🧭 Routing

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

Typed path parameters use :name::Type and arrive as a typed tuple (invalid values are 404s). Wildcards capture the rest of the path:

get!(app, "/users/:id::Int",       (req, id)   -> ...)   # id::Int
get!(app, "/price/:val::Float64",  (req, val)  -> ...)   # val::Float64
get!(app, "/posts/:slug",          (req, slug) -> ...)   # slug::String
get!(app, "/files/*path",          (req, path) -> ...)   # wildcard

Groups share a prefix and scoped middleware (composed as global β†’ group β†’ route at dispatch time):

api = group("/api/v1", middleware=[
    ratelimit(max_requests=100, window_seconds=60),
    apikey("key-abc"; header_name="x-api-key"),
])

get!(api, "/users", list_users)
post!(api, "/users", create_user)

group!(api, "/admin", middleware=[bearer("admin-token")]) do admin
    delete!(admin, "/users/:id::Int", delete_user)
end

mount!(app, api)

Compiled dispatch β€” register everything, then freeze! to close and compile the route table (later registration throws). The closed table is the foundation for AOT builds; juliac --trim compatibility is still in progress:

freeze!(app)     # or freeze!(router) before App(router=router)

🧩 Middleware

Any callable (req, next) β†’ Response is middleware β€” no subtyping needed. Built-ins cover the common production stack:

use!(app, security())                                   # OWASP headers
use!(app, health())                                     # /healthz /readyz /livez
use!(app, metrics())                                    # Prometheus /metrics
use!(app, cors(origins="https://myapp.com"))            # CORS + preflight
use!(app, compress(min_size_bytes=1024))                # gzip
use!(app, etag())                                       # ETag + 304/412
use!(app, logger())                                     # access logs
use!(app, ratelimit(max_requests=100, window_seconds=60))
use!(app, bearer("secret"); paths=["/api"])             # path-scoped auth
use!(app, apikey(["key-abc", "key-xyz"]))
use!(app, basicauth("admin", ENV["ADMIN_PASSWORD"]))
serve!(app, "public"; uri_prefix="/static")             # C-level static files

Custom middleware β€” a closure or a small type:

use!(app) do req, next
    t = time()
    res = next()
    @info "$(req.method) $(req.uri)" status=res.status ms=round((time()-t)*1000; digits=1)
    res
end

πŸ“¨ Request & Response

body(req)                  # raw body
parsejson(req)             # JSON β†’ Dict/Array/...
parseform(req)             # urlencoded body β†’ Dict
parsemultipart(req)        # multipart body β†’ Dict/MultipartFile
parsecookies(req)          # Cookie header β†’ Dict
parsequery(req)            # whole query Dict (query(req, k) for typed lookups)
validate(req, CreateUser)  # parse + coerce + validate into a struct
query(req, "page", 1)      # typed query param with default
header(req, "authorization")
context(req)               # per-request Dict{Symbol,Any}
json(Dict("ok" => true))                  # 200, application/json
json(data; status=201, headers=["X-H" => "v"])
text("Hello!")                            # text/plain
html("<h1>Hi</h1>")
redirect("/new"; status=301)
Response(Json, """{"raw":true}""")        # pre-serialized body

Errors can be thrown or handled by status/type:

throw(NotFoundError("user 7"))            # β†’ 404 automatically

onerror!(app, 404) do req
    json(Dict("error" => "not found"); status=404)
end

onerror!(app, AccountGone) do req, e      # typed exception handler
    json(Dict("error" => "gone"); status=410)
end

πŸ“‘ WebSocket & SSE

ws!(app, "/chat";
    allowed_origins = ["https://myapp.com"],   # optional
    on_open         = req -> true,             # false β†’ 403
    on_message      = msg -> Message("Echo: $(msg.data)"),
    on_close        = () -> @info "disconnected",
)

# Server-initiated push to every client of a path (thread-safe, any task):
broadcastws(app, "/chat", "Server announcement")

Slow WS readers are protected too: pushes beyond send_buffer_bytes (default 1 MiB) unsent are dropped (not buffered), and mongoose_ws_frames_dropped counts them β€” clients resync on reconnect.

Note: unmasked client frames are tolerated (Mongoose's parser does not enforce RFC 6455 Β§5.1 masking). Browsers always mask and there is no server-side security impact.

get!(app, "/events") do req
    sse(req) do writer
        for i in 1:10
            emit(writer; data="Tick $i", event="update", id=string(i))
            sleep(1)
        end
    end
end

βš™οΈ Configuration

app = App(;
    workers            = 4,          # 0 = sync (inline); N = worker pool
    queue_size         = 1024,       # pending requests before 503
    request_timeout_ms = 5_000,      # 0 = disabled (async mode)
    drain_timeout_ms   = 5_000,      # graceful shutdown budget
    max_body_bytes     = 1_048_576,
    header_timeout_ms  = 0,          # close conns that stall before headers
    body_timeout_ms    = 0,          # max time to receive a request body
    max_header_bytes   = 64 * 1024,  # request-header cap
    send_buffer_bytes  = 1_048_576,  # unsent bytes/conn (streams + WS)
    max_bg_tasks       = 0,          # 0 = auto (4Γ—workers); runaway cap
    max_connections    = 0,          # 0 = unlimited
    ws_max_frame_bytes = 1_048_576,
    ws_idle_timeout_ms = 0,          # 0 = disabled
    tls                = nothing,    # TLSConfig(cert=…, key=…)
)

Units are explicit in every name: _ms for timeouts, _seconds for protocol durations, _bytes for sizes. Optional values are disabled with nothing.

HTTPS is native:

app = App(; tls = TLSConfig(cert="path/to/cert.pem", key="path/to/key.pem"))
start!(app; port=8443)

πŸ›‘οΈ Production

  • Graceful shutdown β€” SIGINT and SIGTERM drain in-flight requests and SSE streams, run onstop! hooks, and stop workers (SIGTERM and normal exits go through Julia's atexit path; SIGINT is caught while start! blocks).
  • Backpressure β€” the async executor bounds its queue and answers 503 when full; max_connections, header_timeout_ms, body_timeout_ms, and max_header_bytes protect against slow clients and oversized requests.
  • Observability β€” logger() access logs, metrics() (request counters, latency histogram, and live gauges: connections, WS clients, streams, executor depth), and health() probes for Kubernetes.
  • Errors β€” onerror! per status or per exception type; typed HTTPError{status} aliases map to responses automatically.
  • Introspection β€” isrunning(app), url(app), length(app), matchroute(app, …), hasroute(app, path).
onstart!(app) do; @info "starting"; connect_database!() end
onstop!(app)  do; @info "stopping"; close_database!() end

background!(app) do
    while true
        cleanup_expired_sessions!()
        sleep(60)
    end
end

Dependency injection is a typed NamedTuple; withservices gives type-stable access inside the closure:

app = App(; services=(db=connect_to_database(), cache=RedisPool()))

get!(app, "/users") do req
    users = withservices(req) do svcs
        fetch_users(svcs.db)
    end
    json(users)
end

πŸ§ͺ Testing Without a Server

FakeTransport runs the full pipeline β€” middleware, routing, serialization β€” with no network and no FFI:

using Test, Mongoose

app = App()
get!(app, "/hello", req -> json(Dict("msg" => "hi")))
use!(app, cors())

client = FakeTransport(app)
resp = client(:get, "/hello")
@test resp.status == 200
@test contains(resp.body, "\"msg\"")

For deterministic async tests, FakeExecutor queues jobs on submit! and runs them only when you call run!, in submission order:

fe = FakeExecutor()
submit!(fe, () -> "work")   # enqueued, not run
@test run!(fe) == ["work"]  # FIFO, inline, no threads

πŸ”Œ Pluggable Components

Each boundary is a replacement point: App(router=my_router), App(workers=n) chooses the executor, and AbstractTransport declares its capabilities (canws, cantls, canstream). Custom routers implement route!/matchroute/hasroute and may carry their own endpoint type via invokeendpoint. See the API reference for the exact contracts.


πŸ“š Documentation & Examples

License

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

About

Julia package to build simple web servers

Resources

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

0