web_jvm

The JVM server for `packages/web`: jdk.httpserver underneath, one virtual thread per request. It is the only part of the web framework with a use java; packages/web is pure Dawn, so the native compiler can check, document and test it.

use web/types.{Request, Response, HttpError, text, param}
use web/router.{route_get}
use web/middleware.{with_logging}
use web_jvm/server.{serve_app}

fn hello(req: Request) -> Result[Response, HttpError] !io = {
  let name = param(req, "name")?
  Ok(text(200, "hello, ${name}\n"))
}

pub fn main() -> Unit !io =
  serve_app(8001, [route_get("/hello/{name}", hello)], [with_logging])
[deps]
web = "../packages/web"
web_jvm = "../packages/web-jvm"

What is in web_jvm/server

start(cfg, routes, middleware)binds and answers a ServerHandle; join waits on it, stop ends it, handle_port reads the bound port (for port: 0)
serve_app(port, routes, middleware), serve_app_with(cfg, routes, middleware)start and join in one call
ServerConfighost, port, max_body, and the ErrorFormat for the errors the server words itself
input_source(stream)a web/types.BodySource over a Java InputStream, for streaming(...)

start refuses a route table it cannot dispatch and a max_body that is not positive, before it binds. A handler that panics, or fails with a JVM Error, gets a 500 if nothing was sent yet, and the exchange is always closed. Everything else (requests, responses, routing, guards, body ceilings, CORS, error wording) is documented in packages/web.

This package starts at 1.0.0 with the server API web 6 had in web/server; see packages/web/CHANGELOG.md, 7.0.0, for the migration.

1.1.0

A request body the JDK's stream cannot read (a chunk size that is not hex, a chunk cut short, a body that ends before its Content-Length) is answered 400 with {"error":"malformed request body"}. It was a 500: the exception went up as a panic. A temp file for a spilled body is still removed. The same failures get the same answer from web/exchange (the single-exchange server of packages/web), and scripts/web-exchange-contract holds the two servers to the same bytes. The server now calls the shared decisions that web 7.1 made public (serve_through, body_ceiling, framing, pump_with and the rest) instead of keeping its own copies. Needs web 7.1.

API

web_jvm/server

ServerConfig

type ServerConfig = { host: String, port: Int, max_body: Int, errors: ErrorFormat }

Where the server listens, the in-memory body ceiling its requests get (a positive byte count; start panics on anything else, since there is no "unbounded" — routes that need more opt out with the "raw-body" or "stream-body" tag), and the wording of the errors it renders itself. port 0 asks the OS for a free port; handle_port reports the one actually bound. default_errors() is the neutral wording — an application whose clients expect another error shape states it here (web/types.ErrorFormat).

ServerHandle

type ServerHandle

A running server, as start returns it: join blocks on it, stop ends it, and handle_port reads the port it actually bound (the point of port 0).

server.input_source

fn input_source(s: InputStream) -> BodySource

A BodySource over a Java InputStream: what a JVM program passes to streaming when its upstream is a stream (an HttpClient response body, a file). A read(n) returns what the stream has ready, not n bytes: it waits for one byte, then takes whatever else is already buffered up to n, which is read(byte[])'s behaviour. readNBytes(n) alone would wait to fill the whole chunk, and a download proxied from a slow upstream would stall until 8 KiB had arrived instead of passing each piece on, which is the first-byte-out-immediately property the streaming body exists for.

server.handle_port

fn handle_port(h: ServerHandle) -> Int

The port the server actually bound — with port 0 in the config, the one the OS picked.

server.start

fn start(cfg: ServerConfig, routes: List[Route], middleware: List[fn(fn(Request) -> Result[Response, HttpError] !io) -> fn(Request) -> Result[Response, HttpError] !io]) -> ServerHandle !io

Bind cfg.host:cfg.port, serve requests on virtual threads, and return at once with the handle — the caller decides what the program does next (join to block, stop from wherever shutdown is decided). A broken route table panics here, before the socket is bound.

server.join

fn join(h: ServerHandle) -> Unit !io

Block until stop() releases this handle. A server's main() usually ends here — returning would let the JVM exit out from under the request threads.

server.stop

fn stop(h: ServerHandle, grace_secs: Int) -> Unit !io

Stop the server: the listening socket closes at once, in-flight exchanges get up to grace_secs to finish (the JDK's stop blocks for that), the request executor is shut down, and then every join() blocked on this handle returns. Every one of those happens even if the JDK's own stop throws.

server.serve_app

fn serve_app(port: Int, routes: List[Route], middleware: List[fn(fn(Request) -> Result[Response, HttpError] !io) -> fn(Request) -> Result[Response, HttpError] !io]) -> Unit !io

The old shape, kept: start + join on 127.0.0.1, default body ceiling, default error wording. Most programs want exactly this.

server.serve_app_with

fn serve_app_with(cfg: ServerConfig, routes: List[Route], middleware: List[fn(fn(Request) -> Result[Response, HttpError] !io) -> fn(Request) -> Result[Response, HttpError] !io]) -> Unit !io

start + announce + join under a config the caller owns — the entry point for a program that configures anything (host, error wording) and still wants main() to end by blocking.