eggserve

EggServe is a hardened, HTTP-correct static file server, plus a reusable
static-serving library for Python and Rust. It is a secure alternative to
python -m http.server with the same mental model and safe defaults.
Safe defaults: loopback bind, path confinement, no symlinks, no dotfiles, no
directory listing. Broader behavior is always an explicit opt-in. See
security policy.
Install
pip install eggserve cargo install --path crates/eggserve-bin
Quick start
Python
Static files with the familiar http.server shape:
from functools import partial
from eggserve.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
handler = partial(SimpleHTTPRequestHandler, directory="public")
with ThreadingHTTPServer(("127.0.0.1", 8000), handler) as server:
server.serve_forever()
A custom handler on the same runtime (bounded, synchronous):
from eggserve.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"ok\n" if self.path == "/health" else b"not found\n"
status = 200 if self.path == "/health" else 404
self.send_response(status)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
with ThreadingHTTPServer(("127.0.0.1", 8000), Handler) as server:
server.serve_forever()
A handler-only service without the http.server facade:
from eggserve import lowlevel
def handler(request):
if request.path == "/health":
return lowlevel.Response.text(200, "ok\n")
return lowlevel.Response.text(404, "not found\n")
server = lowlevel.Server(
config=lowlevel.RuntimeConfig(bind="127.0.0.1", port=8000),
handler=handler,
)
server.start()
server.wait_ready()
See Python API and the
compatibility contract for
intentional http.server deviations.
Rust
Add the smallest crate you need (eggserve-core for the composed server):
[dependencies]
eggserve-core = "0.2"
tokio = { version = "1", features = ["full"] }
Serve a confined static directory:
use eggserve_core::server::{RuntimeConfig, Server};
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let server = Server::builder()
.runtime(RuntimeConfig::builder()
.bind("127.0.0.1:8000".parse()?)
.build()?)
.static_service("public")?;
let handle = server.start().await?;
handle.ready().await?;
# Ok(())
# }
Serve a custom service instead:
use eggserve_core::primitives::canonical::{Response, ResponseBody, StatusCode};
use eggserve_core::server::{service_fn, Request, RuntimeConfig, Server, ServiceError};
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let server = Server::builder()
.runtime(RuntimeConfig::builder()
.bind("127.0.0.1:8000".parse()?)
.build()?)
.build()?;
let service = service_fn(|request: Request| async move {
let body = b"ok\n";
Response::builder()
.status(StatusCode::OK)
.body(ResponseBody::Bytes(body.to_vec()))
.map_err(|e| ServiceError::internal(e.to_string()))
});
let handle = server.start_with_service(service).await?;
handle.ready().await?;
# Ok(())
# }
H1 is the supported transport; H2/H3 are opt-in and experimental. See
public API boundary and
downstream app servers.
CLI
eggserve --directory ./public eggserve --directory ./public --public --port 8080
See CLI reference.
Examples
Runnable starting points in examples/:
- Python: static facade, custom handler,
lowlevel service, HTTPS
- Rust: static server, custom service, streaming service, caller-owned stream
- CLI: local static server, explicit public bind, directory listing opt-in
Documentation