Skip to content

ehsanmok/flare

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

241 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

flare

flare

CI Fuzz Docs License: MIT

A Mojo HTTP server you can put in front of users, plus the raw TCP, UDP, TLS, DNS, HTTP/2, and WebSocket primitives it's built on. One reactor per worker (kqueue on macOS, epoll on Linux with EPOLLEXCLUSIVE shared listener for multi-worker), a per-connection state machine, an RFC 7230 parser with extensive fuzz coverage, and a Handler trait that takes plain def functions or compiled-down structs.

from flare import HttpServer, Router, Request, Response, ok, SocketAddr

def hello(req: Request) raises -> Response:
    return ok("hello")

def main() raises:
    var r = Router()
    r.get("/", hello)
    var srv = HttpServer.bind(SocketAddr.localhost(8080))
    srv.serve(r^, num_workers=4)

The bar isn't "is it fast", it's is it hard to misuse under load and easy to operate.

Features

  • Full networking stack: HTTP/1.1 (client + server), HTTP/2 frame codec + HPACK + h2c upgrade, WebSocket (RFC 6455), TLS 1.2/1.3 (client + server, OpenSSL with ALPN), TCP, UDP, DNS, all layered so each module imports only from below.
  • One reactor per worker (kqueue on macOS, epoll on Linux + EPOLLEXCLUSIVE shared listener) with a per-connection state machine + timer wheel; thread-per-core via HttpServer.serve(handler, num_workers=N). Optional cross-worker WorkerHandoffPool (FLARE_SOAK_WORKERS=on) for skewed-keepalive workloads.
  • Composable handlers: Handler trait, Router with path params, App[S] for shared state, typed extractors (PathInt / QueryInt / HeaderStr / Form / Multipart / Cookies / ...), middleware stack (Logger, RequestId, Compress, CatchPanic), Cors, FileServer with HEAD + Range; ComptimeRouter[ROUTES] unrolls dispatch at compile time.
  • Sessions + signed cookies: HMAC-SHA256 (flare.crypto) under typed Session[T] with CookieSessionStore + InMemorySessionStore and the signed_cookie_* lower-level codec.
  • Content negotiation: gzip + brotli content-encoding (RFC 9110 §12.5.3 q-value parser), urlencoded + multipart form parsing, RFC 6265 cookie jars, RFC 9110 Range support.
  • Production hygiene: per-request Cancel token (peer FIN, timeout, drain unified), server-side TLS with cert reload + mTLS + ALPN, streaming bodies with backpressure, sanitised 4xx/5xx, graceful drain with per-worker ShutdownReports, 24 h soak harness, 22 fuzz harnesses (5M+ runs, zero known crashes).

Numbers

TFB plaintext (GET /plaintext → 13-byte Hello, World!), wrk2 calibrated-peak harness with --latency (coordinated-omission corrected). Linux AWS EPYC 7R32, wrk2 -t8 -c256 -d30s, 5x30 s measurement runs at 90 %-of-peak. Worker counts spelled out per row. Full methodology + tables in docs/benchmark.md.

Server Workers Req/s (median) p99 (ms) p99.99 (ms)
actix_web (tokio) 4 264,691 2.80 21.61
hyper (tokio multi-thread) 4 221,349 2.82 3.67
axum (tokio multi-thread) 4 201,042 2.82 3.65
flare_mc (shared listener) 4 170,305 2.38 3.11
nginx (worker_processes 1) 1 63,764 2.29 3.03
flare (reactor) 1 56,086 2.70 3.54
Go net/http (GOMAXPROCS=1) 1 35,940 2.92 5.47
  • Apples-to-apples 4-worker comparison. flare_mc, hyper, axum, actix_web all run 4 OS worker threads. flare_mc holds the best p99 / p99.9 / p99.99 of the four (2.38 / 2.73 / 3.11 ms) and lands at 64 % of the throughput leader (actix_web).
  • Per-core baseline. flare, nginx, go_nethttp all run 1 OS worker thread. flare 1w is 88 % of nginx 1w throughput and 1.56x Go 1w, with comparable tail latency.
  • Multi-worker scaling. flare_mc 4w / flare 1w = 3.04x at the same workload.
  • Apple M-series, single-worker: ~157K req/s, ~1.10x Go net/http.

Install

[workspace]
channels = ["https://conda.modular.com/max-nightly", "conda-forge"]
preview = ["pixi-build"]

[dependencies]
flare = { git = "https://github.com/ehsanmok/flare.git", tag = "<latest-release>" }
pixi install

Requires pixi (pulls Mojo nightly automatically). Released tags are listed on GitHub Releases. Pin to one for reproducible builds.

To track unreleased work (breaking changes possible between tags):

[dependencies]
flare = { git = "https://github.com/ehsanmok/flare.git", branch = "main" }

Quick start

The tour below grows the snippet at the top of this README out, one persona at a time. Each level adds roughly one concept; everything compiles, and the runnable equivalents live under examples/ (every one is part of pixi run tests). docs/cookbook.md maps "I want to..." to the right example, and the rendered package docstring is at https://ehsanmok.github.io/flare/.

Beginner: your first router

Two routes, one with a path parameter, a JSON-shaped response. This is where most apps start: def handlers, a Router, HttpServer.bind, num_workers. No traits, no generics, no extractors yet.

from flare import HttpServer, Router, Request, Response, ok, SocketAddr

def home(req: Request) raises -> Response:
    return ok("flare is up")

def greet(req: Request) raises -> Response:
    return ok("hello, " + req.param("name"))

def health(req: Request) raises -> Response:
    var resp = ok('{"status":"ok"}')
    resp.headers.set("Content-Type", "application/json")
    return resp^

def main() raises:
    var r = Router()
    r.get("/",           home)
    r.get("/hi/:name",   greet)
    r.get("/health",     health)

    var srv = HttpServer.bind(SocketAddr.localhost(8080))
    srv.serve(r^, num_workers=4)

What you get for free: 404 on unknown paths, 405 with Allow on wrong method, sanitised 4xx / 5xx bodies, peer-FIN cancellation, RFC 7230 size limits, the per-worker reactor with kqueue / epoll.

For request bodies, query strings, cookies, sessions, multipart forms, gzip / brotli, TLS, HTTP/2, and WebSocket: all under examples/ (05_http_get, 13_cookies, 28_forms, 29_multipart_upload, 30_sessions, 34_brotli, 12_tls, 35_http2, 06_websocket_echo).

Intermediate: typed extractors

Once your handlers need to read structured input (path params as integers, query strings as bools, headers as strings), promote each Handler from a def into a struct whose fields are the inputs. PathInt["id"] / PathStr / QueryInt / HeaderStr / Form[T] / Multipart / Cookies / ... parse and validate at extraction time; Extracted[H] reflects on the struct's fields and pulls each one in before serve runs. Missing or malformed values become a 400 with a sanitised body, so your serve only sees well-typed values.

from flare.http import (
    Router, ok, Request, Response, HttpServer,
    Extracted, PathInt, Handler,
)
from flare.net import SocketAddr

def home(req: Request) raises -> Response:
    return ok("home")

@fieldwise_init
struct GetUser(Copyable, Defaultable, Handler, Movable):
    var id: PathInt["id"]

    def __init__(out self):
        self.id = PathInt["id"]()

    def serve(self, req: Request) raises -> Response:
        return ok("user=" + String(self.id.value))

def main() raises:
    var r = Router()
    r.get("/", home)
    r.get[Extracted[GetUser]]("/users/:id", Extracted[GetUser]())
    HttpServer.bind(SocketAddr.localhost(8080)).serve(r^, num_workers=4)

Middleware is the same shape: a Handler that wraps another Handler. The stock layers (Logger, RequestId, Compress, CatchPanic, Cors, FileServer) all compose by nesting structs, no callback chain. examples/18_middleware.mojo walks through the production-shaped pipeline (RequestID → Logger → Timing → Recover → RequireAuth → Router).

Advanced: compile-time dispatch, shared state, cancel awareness

Three patterns the production server leans on. Each is independent; pick the one your workload needs.

Cancel-aware handlers. CancelHandler.serve(req, cancel) gets a token the reactor flips on peer FIN, deadline elapse, or graceful drain. Long-running handlers poll between expensive steps and return early; plain Handlers ignore the token and run to completion. The reactor still tears down the connection if the peer goes away; the token just lets your handler do partial work cleanly.

from flare.http import CancelHandler, Cancel, Request, Response, ok

@fieldwise_init
struct SlowHandler(CancelHandler, Copyable, Movable):
    def serve(self, req: Request, cancel: Cancel) raises -> Response:
        for i in range(100):
            if cancel.cancelled():
                return ok("partial: " + String(i))
            # ...one expensive step...
        return ok("done")

Compile-time route tables. When the route table is known at build time, ComptimeRouter[ROUTES] parses the path patterns at compile time and unrolls the dispatch loop per route. No runtime trie walk, no per-request handler-table indirection. Same path-param + wildcard syntax as the runtime Router, same 404 / 405-with-Allow semantics; the only difference is when the dispatch is decided.

from flare.http import (
    ComptimeRoute, ComptimeRouter, HttpServer,
    Request, Response, Method, ok,
)
from flare.net import SocketAddr

def home(req: Request) raises -> Response:
    return ok("home")

def get_user(req: Request) raises -> Response:
    return ok("user=" + req.param("id"))

def files(req: Request) raises -> Response:
    return ok("files=" + req.param("*"))

comptime ROUTES: List[ComptimeRoute] = [
    ComptimeRoute(Method.GET,  "/",            home),
    ComptimeRoute(Method.GET,  "/users/:id",   get_user),
    ComptimeRoute(Method.GET,  "/files/*",     files),
]

def main() raises:
    var r = ComptimeRouter[ROUTES]()
    HttpServer.bind(SocketAddr.localhost(8080)).serve(r^, num_workers=4)

App state + middleware composition. App[S] carries shared state alongside an inner handler; state_view() hands out a borrow that middleware can read or mutate. The compiler monomorphises the whole nested chain into one direct call sequence per request type, with no virtual dispatch and no per-request allocation.

from flare.http import App, Router, Request, Response, Handler, State, ok, HttpServer
from flare.net import SocketAddr

@fieldwise_init
struct Counters(Copyable, Movable):
    var hits: Int

def home(req: Request) raises -> Response:
    return ok("home")

@fieldwise_init
struct WithHits[Inner: Handler](Handler):
    var inner:    Self.Inner
    var snapshot: State[Counters]

    def serve(self, req: Request) raises -> Response:
        var resp = self.inner.serve(req)
        resp.headers.set("X-Hits", String(self.snapshot.get().hits))
        return resp^

def main() raises:
    var router = Router()
    router.get("/", home)
    var app  = App(state=Counters(hits=0), handler=router^)
    var view = app.state_view()

    var srv = HttpServer.bind(SocketAddr.localhost(8080))
    srv.serve(WithHits(inner=app^, snapshot=view^))

For the static-response fast path (serve_static), serve_comptime[handler, config] with build-time invariant checks, the multi-worker shared-listener mode (HttpServer.serve(handler, num_workers=N)), and the cross-worker WorkerHandoffPool (FLARE_SOAK_WORKERS=on), see docs/cookbook.md and the linked examples.

Low-level API

flare ships the primitives the HTTP server is built on, so you can drop down a layer when HTTP isn't the right shape: custom binary protocols, raw TLS, UDP, or running the reactor directly.

from flare.tcp import TcpStream
from flare.tls import TlsStream, TlsConfig
from flare.udp import UdpSocket
from flare.ws  import WsClient
from flare.dns import resolve
from flare.runtime import Reactor, INTEREST_READ

Round-trip examples for each (04_tcp_echo, 06_websocket_echo, 11_udp, 12_tls, 14_reactor) live under examples/ and the rendered package docstring at https://ehsanmok.github.io/flare/ walks the layered API top-down. Use cases: a custom protocol over TLS, a UDP client / server, a WebSocket client driven from a CLI tool, or a hand-rolled non-HTTP server on top of the same reactor that powers HttpServer.

Architecture

flare.io       BufReader (Readable trait, generic buffered reader)
flare.ws       WebSocket client + server (RFC 6455)
flare.http     HTTP/1.1 client + reactor server + Cancel + Handler / Router / App
flare.tls      TLS 1.2/1.3 (OpenSSL, both client and server; reactor-loop integration follow-up)
flare.tcp      TcpStream + TcpListener (IPv4 + IPv6)
flare.udp      UdpSocket (IPv4 + IPv6)
flare.dns      getaddrinfo (dual-stack)
flare.net      IpAddr, SocketAddr, RawSocket
flare.runtime  Reactor (kqueue/epoll), TimerWheel, Scheduler, Pool[T]

Each layer imports only from layers below it. No circular dependencies. The full request lifecycle, including the Cancel injection point and the per-connection state machine, lives in docs/architecture.md.

Performance

Headline numbers live in the Numbers block above; full single/multi-worker tables, tail percentiles, methodology, and the soak harness for long-running operational gates live in docs/benchmark.md. Multi-worker cross-server ratios are gated on matched-worker baselines (Go GOMAXPROCS=N, nginx worker_processes N, Rust hyper / axum N-worker); flare publishes its own scaling claim and does not publish a "vs Go" multicore ratio against single-worker baselines.

We do not lead on speed. The position is plain: speed claims in networking are mostly architecture and kernel, not language. flare's job is to be operationally honest under load. Numbers are a corollary, not the headline.

Security

Per-layer security posture and the sanitised-error-response policy live in docs/security.md. Highlights: RFC 7230 token validation, configurable size limits, sanitised 4xx/5xx bodies, TLS 1.2+ only, WebSocket frame masking + UTF-8 validation, 19 fuzz harnesses with 4M+ runs and zero known crashes.

For security issues, please open a private security advisory on GitHub or email the maintainer directly.

Develop

git clone https://github.com/ehsanmok/flare.git && cd flare
pixi install                  # lean: tests, examples, microbench, format-check
pixi install -e dev           # adds mojodoc + pre-commit

flare uses four pixi environments, layered:

Env Adds What it unlocks
default nothing tests, examples, microbenchmarks, format-check
dev mojodoc, pre-commit docs, docs-build, format (with hook install)
fuzz dev + mozz fuzz-* / prop-*
bench dev + go, nginx, wrk, wrk2 bench-vs-baseline*, bench-tail-quick, bench-mixed-keepalive
pixi run tests                                     # full suite + 21+ examples
pixi run --environment fuzz fuzz-all               # 19 harnesses
pixi run --environment bench bench-vs-baseline-quick   # ~7 min

Per-component task list lives in pixi.toml. The full architecture / benchmark / security / cookbook tour is under docs/.

License

MIT

About

Full Networking Stack for Mojo🔥

Resources

License

Security policy

Stars

Watchers

Forks

Contributors