[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"cEOPcPsonB":3},"\u003Cdiv align=\"center\">\n\n# LeanIO\n\n[![Lean](https://img.shields.io/badge/Lean-4.33.0-0f4c81)](https://lean-lang.org/)\n[![Lake](https://img.shields.io/badge/build-Lake-blue)](https://github.com/leanprover/lake)\n[![Version](https://img.shields.io/badge/version-0.6.0-2ea44f)](./lakefile.toml)\n[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)\n\nA composable HTTP toolkit for Lean 4. Built on `Std.Http.Server` with an\naxum-inspired extractor DSL, middleware chaining, and sub-router mounting.\n\n\u003C/div>\n\n## Highlights\n\n- ⚡ **Route macros** — 41 HTTP methods as term macros with compile-time pattern validation\n- 🧩 **Extractors** — typed parameters injected into handlers: `Path Nat`, `Json T`, `Query T`, `Form α`, `MultiPartForm`\n- 📤 **Streaming uploads** — zero-copy multipart parser\n- 📁 **File serving** — `File` streams from disk with MIME detection; `RangeFile` adds `206 Partial Content` and `Accept-Ranges`\n- 🏷️ **ETag caching** — `CacheControl` directives with presets; weak ETags for files (mtime+size) and JSON (String.hash)\n- 🧪 **Middlewares** — wraps both request and response; built-in logging, error catching, and auth\n- 🏗️ **Router composition** — segment trie with O(depth) lookup, literal > param > wildcard priority, sub-routers merged at serve time\n- 🎯 **IntoResponse** — return `String`, `Status × T`, `Except ε α`, `File`, or implement your own — all streamed, never buffered\n- 🚀 **Deriving** — `FromPath`, `FromQuery`, `FromForm` auto-generated from struct field names\n\n## Contents\n\n1. [Overview](#1-overview)\n2. [Routes](#2-routes)\n3. [Extractors](#3-extractors)\n4. [Responses](#4-responses)\n5. [Middleware](#5-middleware)\n6. [Router](#6-router)\n7. [Reference](#7-reference)\n\n---\n\n## 1. Overview\n\nThis chapter walks through building a complete application — a REST API for a task list,\nwith a browser frontend served from disk. Every concept is presented in context first;\nlater chapters provide the full API reference.\n\n### 1.1 A complete application\n\n```lean\nimport LeanIO\nopen LeanIO.Router\nopen LeanIO.Middlewares\nopen Std Http Server\nopen Std Async\nopen Lean\n\nset_option linter.unusedVariables false\n\n/- Data model -/\n\nstructure Todo where\n  title     : String\n  completed : Bool := false\nderiving Inhabited, ToJson, FromJson\n\nstructure TodoStore where\n  todos : Array Todo\nderiving Inhabited\n\n/- State (shared via middleware) -/\n\nstructure AppState where\n  ref : IO.Ref TodoStore\nderiving TypeName\n\ninstance : FromRequestParts AppState where\n  from_request_parts req :=\n    match req.extensions.get AppState with\n    | some s => .ok s\n    | none   => .error \"state not installed\"\n\n/- Routes -/\n\ndef listTodos := GET \"/api/todos\" (⟨state⟩ : AppState) => do\n    let store ← state.get\n    return store.todos\n\ndef addTodo := POST \"/api/todos\" (⟨body⟩ : Json Todo) (⟨state⟩ : AppState) => do\n    let store ← state.get\n    state.set { store with todos := store.todos.push body }\n    return (Status.created, body)\n\ndef toggleTodo := PATCH \"/api/todos/{id}\" (⟨state⟩ : AppState) (⟨id⟩ : Path Nat) => do\n    let store ← state.get\n    if h : id \u003C store.todos.size then\n      let t := store.todos[id]!\n      state.set { store with todos := store.todos.set id {t with completed := ¬t.completed} }\n      return Except.ok (Status.ok)\n    else\n      return Except.error (Status.notFound, s!\"todo {id} not found\")\n\ndef deleteTodo := DELETE \"/api/todos/{id}\" (⟨state⟩ : AppState) (⟨id⟩ : Path Nat) => do\n    let store ← state.get\n    if h : id \u003C store.todos.size then\n      state.set { store with todos := store.todos.eraseIdx id }\n      return Except.ok (Status.ok)\n    else\n      return Except.error (Status.notFound, s!\"todo {id} not found\")\n\n/- Frontend — serve a SPA from disk -/\n\ndef serveUI := GET \"/{*rest}\" (⟨rest⟩ : Path String) => do\n    let path : System.FilePath := \"public\" / rest\n    return { path : RangeFile }\n\n/- Entry point -/\n\ndef main : IO Unit := Async.block do\n  let ref ← IO.mkRef { todos := #[] : TodoStore }\n  let router := Router.empty\n    |>.addRoute listTodos\n    |>.addRoute addTodo\n    |>.addRoute toggleTodo\n    |>.addRoute deleteTodo\n    |>.addRoute serveUI\n    |>.addMiddleware (withExtension AppState { ref })\n    |>.addMiddleware catchErrors\n    |>.addMiddleware requestLogger\n  let addr : Net.SocketAddress := .v4 ⟨.ofParts 127 0 0 1, 8080⟩\n  let server ← router.serve addr\n  IO.println \"Listening on http://127.0.0.1:8080\"\n  server.waitShutdown\n```\n\nThis single file contains:\n\n- **Route definitions** — four CRUD endpoints under `/api/todos` and a catch-all\n  that serves static files from the `public/` directory.\n- **Extractors** — `(⟨body⟩ : Json Todo)` deserializes the JSON body; `(⟨id⟩ : Path Nat)`\n  pulls path parameters; `(⟨state⟩ : AppState)` destructures the middleware state to\n  the `IO.Ref` directly; `(⟨rest⟩ : Path String)` captures the wildcard path.\n- **Responses** — handlers return `ToJson` values, `Status × T` tuples, or `Except`\n  for fallible results. `{ path : RangeFile }` streams files from disk with HTTP Range\n  support for video and partial requests.\n- **Middleware** — `withExtension` installs the shared store; `catchErrors` catches\n  exceptions from downstream; `requestLogger` logs every request with its response\n  status code and timing.\n\n### 1.2 The router pipeline\n\n`Router.serve` compiles the router into a `RouteTrie`, pre-composing every\nmiddleware chain around its handler. At request time the trie looks up the\nmatching pipeline and runs it — no composition happens per request. The\nresponse flows back through every middleware before being returned:\n\n```mermaid\nsequenceDiagram\n  actor C as Client\n  participant S as Server\n  participant R as RouteTrie\n  participant M1 as requestLogger\n  participant M2 as catchErrors\n  participant H as Handler\n  C->>S: TCP request\n  S->>R: onRequest\n  activate R\n  R-->>R: lookup\n  R->>M1: middleware chain\n  activate M1\n  M1->>M2: next req\n  activate M2\n  M2->>H: next req\n  activate H\n  H-->>M2: response\n  deactivate H\n  M2-->>M1: return\n  deactivate M2\n  M1-->>R: return\n  deactivate M1\n  R-->>S: response\n  deactivate R\n  S-->>C: TCP response\n```\n\nMiddlewares are composed around the handler at serve time with `foldl`\n(last added runs outermost). Each middleware sees the request on the way in\nand the response on the way out.\n\nExtractors run as part of handler invocation — route parameters are captured from\nthe path during trie lookup and stored in request extensions. Body extractors read\nfrom the underlying `Body.Stream`.\n\n### 1.3 What's next\n\n| To...                                                          | Read chapter                   |\n| -------------------------------------------------------------- | ------------------------------ |\n| Define routes with path parameters, compile-time validation    | [2. Routes](#2-routes)         |\n| Extract path params, JSON bodies, forms, file uploads, queries | [3. Extractors](#3-extractors) |\n| Return strings, JSON, status codes, files, cached responses    | [4. Responses](#4-responses)   |\n| Add logging, error handling, auth, shared state                | [5. Middleware](#5-middleware) |\n| Compose sub-routers under path prefixes                        | [6. Router](#6-router)         |\n| All HTTP methods, utility types, examples                      | [7. Reference](#7-reference)   |\n\n---\n\n## 2. Routes\n\nA route is a value of type `Route`. It pairs an HTTP method, a path pattern, and a\nhandler function. Routes are created via **term macros** — the macro expands pattern\nsyntax, validates parameters at compile time, and wraps the handler with extractor logic.\n\n### 2.1 Route macro\n\n```\nMETHOD \"pattern\" extractor ... extractor => handler-body\n```\n\n- `METHOD` — HTTP verb: `GET`, `POST`, `PUT`, etc.\n- `\"pattern\"` — a string literal starting with `/`, possibly containing path parameters.\n- `extractor` — one or more `(⟨name⟩ : Type)` binders (see chapter 3).\n- `handler-body` — a `ContextAsync R` or `R` expression where `R` implements `IntoResponse`.\n\nThe macro expands to a `Route` value:\n\n```lean\nstructure Route where\n  method      : Method\n  pat         : RoutePattern\n  handler     : HandlerFn\n  middlewares : List Middleware := []\n```\n\n#### Pattern syntax\n\n| Syntax    | Segment       | Extractor                       |\n| --------- | ------------- | ------------------------------- |\n| `/todos`  | `lit \"todos\"` | —                               |\n| `{id}`    | `param \"id\"`  | `Path Nat`, `Path String`, etc. |\n| `{*rest}` | `rest \"rest\"` | `Path String`                   |\n\nParam names must start with a letter or underscore and contain only alphanumeric\ncharacters or underscores. The macro rejects invalid patterns at compile time.\n\n```lean\n-- ✓ Valid\nGET \"/user/{id}\" ...\nGET \"/posts/{year}/{month}\" ...\nGET \"/files/{*path}\" ...\n\n-- ✗ Compile-time error\nGET \"no-slash\" ...            -- must start with /\nGET \"/{*rest}/suffix\" ...     -- rest must be last segment\nGET \"/{3bad}\" ...             -- param name cannot start with a digit\n```\n\nMultiple parameters are extracted by position unless a named struct is used\n(see §3.1.3).\n\n```lean\nGET \"/a/{p1}/b/{p2}\" (⟨x, y⟩ : Path (Nat × String)) ...  -- positional\nGET \"/a/{p1}/b/{p2}\" (⟨ids⟩     : Path TodoIds) ...       -- by field name\n```\n\n### 2.2 Adding routes to a router\n\nRoutes are added to a `Router` via the pipe-builder combinator `.addRoute`:\n\n```lean\nRouter.empty\n  |>.addRoute listTasks\n  |>.addRoute addTask\n```\n\nEach route is compiled into the trie at serve time. See chapter 6 for details on\nrouter composition and sub-router mounting.\n\n### 2.3 Inline routes\n\nRoutes do not need to be named:\n\n```lean\nRouter.empty\n  |>.addRoute (GET \"/healthz\" => \"ok\")\n  |>.addRoute (POST \"/echo\" (⟨body⟩ : Json Nat) => return body)\n```\n\n---\n\n## 3. Extractors\n\nAn extractor is a typed parameter of the form `(⟨name⟩ : Type)` declared after the route\npattern. Extractors supply data to the handler — path segments, JSON bodies, query\nstrings, headers, or custom values — and can be freely composed.\n\nThe extractor system is built on two classes:\n\n```lean\nclass FromRequestParts (α : Type) where\n  from_request_parts : Request Body.Stream → Except String α\n\nclass FromRequestBody (α : Type) where\n  from_request_body : Request Body.Stream → ContextAsync (Except String α)\n```\n\n`FromRequestParts` runs synchronously from request metadata (path, headers, query,\nextensions). `FromRequestBody` runs asynchronously from the body stream. Extractors\ncombine these two classes — at most one body extractor is allowed per handler, and\nit must be declared first.\n\n### 3.1 Path parameters\n\n```lean\nstructure Path (α : Type) where\n  value : α\n```\n\nPath parameters are deserialized with `FromString` (for scalar types) or `FromPath`\n(for structs). The route pattern captures segments into a `HashMap String String`\nstored in request extensions, and the extractor reads them back.\n\n#### 3.1.1 Scalar types\n\nBuilt-in `FromString` instances: `String`, `Nat`, `Int`, `Bool`.\n\n```lean\ndef getItem := GET \"/items/{id}\" (⟨id⟩ : Path Nat) => do\n    return itemsDb.find id\n\ndef greet   := GET \"/hello/{name}\" (⟨name⟩ : Path String) =>\n    s!\"Hello, {name}\"\n```\n\n#### 3.1.2 Tuple types\n\nMultiple path params are extracted as tuples, up to 5 elements:\n\n```lean\ndef getComment := GET \"/todos/{id}/comments/{cId}\" (⟨id, cId⟩ : Path (Nat × Nat)) => do\n    return Comment.find id cId\n\ndef complex  := GET \"/{a}/{b}/{c}/{d}\" (⟨a, b, c, d⟩ : Path (Nat × String × Nat × Bool)) => ...\n```\n\n#### 3.1.3 Named struct parameters\n\nUse `deriving FromPath` on a structure to match path parameters by field name\ninstead of position:\n\n```lean\nstructure TodoIds where\n  id  : Nat\n  cId : Nat\nderiving FromPath\n\ndef getComment := GET \"/todos/{id}/comments/{cId}\" (⟨ids⟩ : Path TodoIds) => do\n    return Comment.find ids.id ids.cId\n```\n\nThe deriving handler generates a `FromPath` instance that looks up each struct field\nby name in the captured route params.\n\n### 3.2 Body extractors\n\n#### 3.2.1 `Json α`\n\nDeserializes the request body as JSON. Requires `FromJson α` and validates\n`Content-Type: application/json`.\n\n```lean\nstructure Json (α : Type) where\n  body : α\n```\n\n```lean\nstructure CreateRequest where\n  title : String\nderiving FromJson\n\ndef create := POST \"/items\" (⟨body⟩ : Json CreateRequest) => do\n    return (Status.created, Item.of body)\n```\n\n#### 3.2.2 `PlainText`\n\nReads the entire body as a `String`. Validates `Content-Type: text/plain`.\n\n```lean\nstructure PlainText where\n  body : String\n```\n\n```lean\ndef echo := POST \"/echo\" (body : PlainText) => do\n    return body\n```\n\n#### 3.2.3 `Form α`\n\nParses `application/x-www-form-urlencoded` bodies. Use `deriving FromForm` to\ndeserialize into a struct:\n\n```lean\nstructure Form (α : Type) where\n  value : α\n```\n\n```lean\nstructure LoginForm where\n  username : String\n  password : String\nderiving FromForm\n\ndef login := POST \"/login\" (⟨form⟩ : Form LoginForm) => do\n    return s!\"logged in as {form.username}\"\n```\n\nFor unstructured access, use `Form (HashMap String String)`.\n\n#### 3.2.4 `MultiPartForm`\n\nStreaming multipart parser for `multipart/form-data`. Consumes the body lazily;\nfile contents are never buffered.\n\n```lean\nstructure MultiPartForm where\n  inner : IO.Ref MultipartInner\n\ninductive MultipartEntry where\n  | field (name : String) (value : String)\n  | file  (file  : FormFile)\n\nstructure FormFile where\n  name        : String\n  filename    : String\n  contentType : String\n  headers     : Std.Http.Headers\n  inner       : IO.Ref MultipartInner\n```\n\n**Lifecycle.** Call `mp.nextEntry` in a loop until it returns `none`. Each entry is\neither a `.field` (in-memory string) or a `.file` (streamed from the body).\n`FormFile` provides four methods for consuming the file body:\n\n| Method     | Signature                                             | Description                   |\n| ---------- | ----------------------------------------------------- | ----------------------------- |\n| `.save`    | `System.FilePath → ContextAsync Unit`                 | Streams chunks to disk        |\n| `.bytes`   | `ContextAsync ByteArray`                              | Reads all chunks into memory  |\n| `.stream`  | `(ByteArray → ContextAsync Unit) → ContextAsync Unit` | Calls a callback per chunk    |\n| `.discard` | `ContextAsync Unit`                                   | Reads and discards all chunks |\n\n```lean\ndef upload := POST \"/upload\" (mp : MultiPartForm) => do\n  while let some entry := ← mp.nextEntry do\n    match entry with\n    | .field name value =>\n      IO.println s!\"field {name} = {value}\"\n    | .file file =>\n      file.save s!\"uploads/{file.filename}\"    -- stream to disk\n      -- file.stream fun chunk => ...           -- per-chunk callback\n      -- let data ← file.bytes                   -- read into memory\n      -- file.discard                            -- skip\n  return Status.ok\n```\n\nUnder the hood, `MultiPartForm` uses a Knuth-Morris-Pratt automaton for\nboundary detection over a zero-copy `ChunkBuffer`, peaking at ~1 MB memory\nregardless of upload size.\n\n### 3.3 Query parameters\n\n```lean\nstructure Query (α : Type) where\n  value : α\n```\n\nParses the request URI query string into a struct via `FromQuery`. Fields with a\ndefault value (`:=`) use that default when the key is missing. `Option T` fields\ndefault to `none`. Other fields produce an error if absent.\n\n```lean\nstructure Pagination where\n  offset : Nat := 0\n  limit  : Nat := 10\nderiving FromQuery\n\ndef listItems := GET \"/todos\" (⟨page⟩ : Query Pagination) => do\n    let store ← db.get\n    return store.items\n      |>.extract page.offset (page.offset + page.limit)\n```\n\n### 3.4 Other built-in extractors\n\nThese `FromRequestParts` instances extract raw request metadata without a wrapper type:\n\n| Extractor       | Type                         | Description           |\n| --------------- | ---------------------------- | --------------------- |\n| `Method`        | `Std.Http.Method`            | HTTP method           |\n| `Version`       | `Std.Http.Version`           | HTTP version          |\n| `Headers`       | `Std.Http.Headers`           | All request headers   |\n| `URI.Path`      | `String`                     | Request path          |\n| `URI.Query`     | `String`                     | Raw query string      |\n| `RequestTarget` | `String`                     | Full request URI      |\n| `HeaderRange`   | `HeaderRange`                | Parsed `Range` header |\n| `RemoteAddr`    | `Std.Http.Server.RemoteAddr` | Remote client IP      |\n\n### 3.5 Custom extractors\n\n#### 3.5.1 From request parts\n\nImplement `FromRequestParts` to extract values synchronously from request metadata:\n\n```lean\ninstance : FromRequestParts ApiKey where\n  from_request_parts req :=\n    match req.line.headers.find? (.mk \"x-api-key\") with\n    | some (_, v) => .ok { key := v }\n    | none        => .error \"missing api key\"\n\ndef secure := GET \"/secure\" (⟨key⟩ : ApiKey) => ...\n```\n\n#### 3.5.2 From request body\n\nImplement `FromRequestBody` to read the body asynchronously:\n\n```lean\ninstance [FromXml α] : FromRequestBody (Xml α) where\n  from_request_body req := do\n    let raw ← req.body.readAll\n    match parseXml raw with\n    | .ok v    => return .ok { body := v }\n    | .error e => return .error e\n\ndef consume := POST \"/xml\" (⟨body⟩ : Xml T) => ...\n```\n\n#### 3.5.3 Sum types\n\n`α ⊕ β` chains two `FromRequestBody` instances. The request's `Content-Type` is matched against the `HasMimeTypes` of each side — the **first** side whose MIME type declares a match for the incoming header wins, and its extractor runs.\n\n**Why use this.** One endpoint, multiple payload formats. A REST API that must\naccept both a typed JSON payload from a rich client **and** an\n`application/x-www-form-urlencoded` form from a browser can use a single\nhandler with a sum body extractor.\n\n```lean4\nstructure CreateUser where\n  name  : String\n  email : String\nderiving FromJson, FromForm, ToJson\n\ndef createUser := POST \"/users\"\n    (body : Json CreateUser ⊕ Form CreateUser) => do\n  let data : CreateUser := match body with\n    | Sum.inl j => j.body\n    | Sum.inr f => f.value\n  pure (Status.created, data)\n```\n\n| `Content-Type` header               | Which side is chosen          | Response                   |\n| ----------------------------------- | ----------------------------- | -------------------------- |\n| `application/json`                  | `Json CreateUser` (`Sum.inl`) | 201 Created                |\n| `application/x-www-form-urlencoded` | `Form CreateUser` (`Sum.inr`) | 201 Created                |\n| anything else (e.g. `text/plain`)   | —                             | 415 Unsupported Media Type |\n\nBoth `Json T` and `Form T` can derive from the same underlying structure — a single\n`deriving` clause covers all the boilerplate. The handler extracts the common `data`\nvia `match` regardless of which format arrived.\n\nYou can chain any pair of body extractors that carry `HasMimeTypes`. For example\n`PlainText ⊕ Json T` dispatches between `text/plain` and `application/json`;\n`MultiPartForm ⊕ Json T` handles file-upload vs JSON on the same URI.\n\n### 3.6 Handler signature rules\n\nThe extractor system supports these handler shapes:\n\n| Shape                                   | Example                                      |\n| --------------------------------------- | -------------------------------------------- |\n| `ContextAsync R` (no extractors)        | `GET \"/ping\" => do ...`                      |\n| `R` (0 params, sync)                    | `GET \"/ping\" => \"pong\"`                      |\n| `BodyExtractor → Rest` (1 body + parts) | `POST \"/todos\" (⟨b⟩ : Json T) => ...`        |\n| `PartsExtractor → Rest` (parts only)    | `GET \"/todos/{id}\" (⟨id⟩ : Path Nat) => ...` |\n\nAt most one body extractor is allowed, and it must appear before any parts extractors.\n\n---\n\n## 4. Responses\n\nEvery handler must return a type implementing `IntoResponse`. Responses are streamed —\nthe framework does not buffer the full body.\n\n```lean\nclass IntoResponse (α : Type) where\n  into_response : ContextAsync α → ContextAsync (Response Body.Any)\n```\n\nA second class, `IntoResponseExt`, receives the request for use cases like ETag matching:\n\n```lean\nclass IntoResponseExt (α : Type) where\n  into_response_ext : Request Body.Stream → ContextAsync α → ContextAsync (Response Body.Any)\n```\n\n### 4.1 Built-in response types\n\n| Return type                              | Status                      | Body                  |\n| ---------------------------------------- | --------------------------- | --------------------- |\n| `String`                                 | `200`                       | `text/plain`          |\n| `Unit` / `()`                            | `200`                       | Empty                 |\n| `IO.Error`                               | `500`                       | Error message         |\n| `Status`                                 | Given status                | Empty                 |\n| `T` (with `ToJson T`)                    | `200`                       | `application/json`    |\n| `Status × String`                        | Given status                | `text/plain`          |\n| `Status × T` (with `ToJson T`)           | Given status                | `application/json`    |\n| `Status × Headers × T` (with `ToJson T`) | Given status                | Custom headers + JSON |\n| `Except ε α`                             | `.ok` → rhs, `.error` → lhs | Delegated             |\n\n```lean\ndef created  := POST \"/items\" ... => do\n    return (Status.created, item)                          -- 201 + JSON\n\ndef deleted  := DELETE \"/items/{id}\" ... => do\n    return Except.ok s!\"Item {id} deleted\"                 -- 200 text/plain\n\ndef notFound := GET \"/items/{id}\" ... => do\n    return Except.error (Status.notFound, { error := \"not found\" })  -- 404 + JSON\n\ndef oops     := GET \"/boom\" =>\n    throw \u003C| IO.userError \"bad\"                           -- 500\n```\n\n### 4.2 File\n\nStreams a file from disk with `Content-Length` framing. MIME type is detected from the\nfile extension.\n\n```lean\nstructure File where\n  path         : System.FilePath\n  cacheControl : Option CacheControl := some \u003C| CacheControl.publicStatic 0\n```\n\n| Field          | Default      | Description              |\n| -------------- | ------------ | ------------------------ |\n| `path`         | *(required)* | Path to the file on disk |\n| `cacheControl` | `some \u003C      | publicStatic 0`          | When `some cc`, sends `ETag`, `Cache-Control`, and supports `304 Not Modified`. When `none`, no caching headers are sent |\n\n`ETag` is a weak validator computed from the file's `mtime` and byte size.\n\n```lean\ndef serveUI := GET \"/static/{*rest}\" (⟨rest⟩ : Path String) => do\n    return { path := \"public\" / rest : File }\n\n-- With custom cache policy:\ndef serveIcons := GET \"/icons/{*rest}\" (⟨rest⟩ : Path String) => do\n    return { path := \"icons\" / rest\n             cacheControl := CacheControl.publicStaticHashed 31536000 : File }\n```\n\n### 4.3 RangeFile\n\nLike `File` but with HTTP `Range` header support. Sets `Accept-Ranges: bytes` and\nresponds with `206 Partial Content` for range requests.\n\n```lean\nstructure RangeFile where\n  path         : System.FilePath\n  cacheControl : Option CacheControl := some \u003C| CacheControl.publicStatic 0\n```\n\n| Range format  | Meaning                  |\n| ------------- | ------------------------ |\n| `bytes=0-499` | Bytes 0 to 499 inclusive |\n| `bytes=500-`  | Bytes 500 to end of file |\n| `bytes=-500`  | Last 500 bytes           |\n\nOut-of-bounds ranges return `416 Range Not Satisfiable`.\n\n```bash\n$ curl -H \"Range: bytes=0-1023\" http://localhost:8080/media/video.mp4\n# → 206 Partial Content\n# → Content-Range: bytes 0-1023/9876543\n```\n\n### 4.4 BrowserCached\n\nWraps any `ToJson α` value with `ETag` and `Cache-Control` headers for revalidation.\nThe handler executes on every request, but on a cache hit (`If-None-Match` matches)\nthe response body is omitted (`304 Not Modified`), saving bandwidth.\n\n```lean\nstructure BrowserCached (α : Type) where\n  value        : α\n  cacheControl : CacheControl := CacheControl.userPrivate\n```\n\nThe ETag is a weak validator computed from `String.hash` of the serialized JSON.\n\n```lean\ndef getTodos := GET \"/todos\" (⟨page⟩ : Query Pagination) => do\n    let todos ← db.find page.offset page.limit\n    return BrowserCached.new todos\n\n-- Override cache control:\ndef getTodosCached := GET \"/todos/cached\" (⟨page⟩ : Query Pagination) => do\n    let todos ← db.find page.offset page.limit\n    return BrowserCached.new todos  (CacheControl.publicStatic 60)\n```\n\n### 4.5 Custom responses\n\nImplement `IntoResponse` to define your own response type:\n\n```lean\ninstance : IntoResponse Html where\n  into_response html := do\n    let h ← html\n    Response.ok\n      |>.header (.mk \"content-type\") (.mk \"text/html\")\n      |>.text (Html.render h)\n\ndef page := GET \"/\" =>\n    Html.renderPage db.users\n```\n\nUse `IntoResponseExt` when the response logic depends on the request (e.g., ETag\nmatching, content negotiation). `File`, `RangeFile`, and `BrowserCached` are\nimplemented via `IntoResponseExt`.\n\n---\n\n## 5. Middleware\n\nMiddleware is a function that wraps the handler pipeline, seeing both the request\non the way in and the response on the way out:\n\n```lean\nabbrev HandlerFn := Request Body.Stream → ContextAsync (Response Body.Any)\n\n-- Middleware type:\nabbrev Middleware := HandlerFn → HandlerFn\n```\n\nAny function of this type qualifies. It receives the next handler in the chain,\ncalls it, and can inspect or modify the response before returning:\n\n```lean\ndef timingMiddleware : Middleware := fun next req => do\n  let start ← IO.monoNanosNow\n  let res ← next req\n  let elapsed ← (· - start) \u003C$> IO.monoNanosNow\n  IO.eprintln s!\"{req.line.method} {req.line.uri.path} → {res.line.status} in {elapsed}ns\"\n  return res\n```\n\nMiddleware can be attached at three levels:\n\n| Level       | Method                       | Scope                        |\n| ----------- | ---------------------------- | ---------------------------- |\n| Route       | `route.addMiddleware mw`     | That route only              |\n| Sub-router  | `subRouter.addMiddleware mw` | All routes in the sub-router |\n| Root router | `router.addMiddleware mw`    | All routes                   |\n\nMiddleware runs in **last-added-first** order: the last middleware added wraps all\nearlier ones. A typical stack:\n\n```lean\nRouter.empty\n  |>.addRoute myRoute\n  |>.addMiddleware auth             -- 3rd (inner)\n  |>.addMiddleware catchErrors      -- 2nd\n  |>.addMiddleware requestLogger    -- 1st (outermost)\n```\n\nMiddleware wraps the entire handler — it sees the request on the way in and the\nresponse on the way out.\n\n```mermaid\nsequenceDiagram\n  participant R as Router\n  participant M1 as requestLogger\n  participant M2 as catchErrors\n  participant M3 as auth\n  participant H as Handler\n  R-->>R: handler lookup\n  R->>M1: call middleware chain\n  activate M1\n  M1->>M2: next req\n  activate M2\n  M2->>M3: next req\n  activate M3\n  M3->>H: next req\n  activate H\n  H-->>M3: response\n  deactivate H\n  M3-->>M2: return\n  deactivate M3\n  M2-->>M1: return\n  deactivate M2\n  M1-->>R: return\n  deactivate M1\n```\n\n### 5.1 Built-in middleware\n\n#### `requestLogger`\n\nLogs `METHOD`, path, status code, and response time to stdout. Reads the status\nfrom the response on the way out.\n```lean\nRouter.empty\n  |>.addMiddleware requestLogger\n```\n\n#### `catchErrors`\n\nWraps downstream middleware and the handler in a `try/catch`. On exception, returns\n`500 Internal Server Error` by default, or calls a custom error handler.\n\n```lean\ndef catchErrors\n    (onError : IO.Error → ContextAsync (Response Body.Any) :=\n      fun _ => Response.internalServerError |>.empty)\n    (next : HandlerFn) : HandlerFn\n```\n\n```lean\nRouter.empty\n  |>.addRoute myRoute\n  |>.addMiddleware (catchErrors fun e =>\n    Response.ok |>.text s!\"custom error: {e}\")\n```\n\n#### `auth`\n\nBasic or bearer token authentication. Returns `401 Unauthorized` with\n`WWW-Authenticate` header on failure.\n\n```lean\ninductive AuthConfig where\n  | basic  (validate : String → Redacted → Async Bool)\n  | bearer (validate : Redacted → Async Bool)\n```\n\n`Redacted` is a string wrapper that hides its value in logs and debug output\n(see §8.2).\n\n```lean\ndef authConfig : AuthConfig := .basic fun username password =>\n    return username == \"admin\" && password.expose == \"secret\"\n\nRouter.empty\n  |>.addRoute protectedRoute\n  |>.addMiddleware (auth authConfig)\n```\n\n\n#### `withExtension`\n\nInjects a value into the request's extension map. Extractors retrieve it later via\n`FromRequestParts`. This is the mechanism for sharing state across routes.\n\n```lean\ndef withExtension (α : Type) [TypeName α] (data : α) : Middleware :=\n  fun next req => next { req with extensions := req.extensions.insert data }\n```\n\n```lean\nstructure AppState where\n  ref : IO.Ref Db\nderiving TypeName\n\ninstance : FromRequestParts AppState where\n  from_request_parts req :=\n    match req.extensions.get AppState with\n    | some s => .ok s\n    | none   => .error \"not installed\"\n\ndef stateMiddleware := do\n  let ref ← IO.mkRef defaultDb\n  return withExtension AppState { ref }\n\n-- Access in handler:\ndef getData := GET \"/data\" (⟨s⟩ : AppState) => do\n    let db ← s.get\n    return db.items\n```\n\n---\n\n## 6. Router\n\nThe router is a **declarative description**: an array of mounted sub-routers, an array\nof routes and an array of router-level middlewares. Nothing is composed at\nregistration time. `Router.serve` (via `Router.toRouteTrie`) compiles the whole\ntree into a **segment trie** for O(depth) dispatch — middlewares are pre-composed\nonto every handler exactly once, and there is no delegation or composition at\ndispatch.\n\n```lean\nstructure Router where\n  routers     : Array (String × Router) := #[]\n  routes      : Array Route := #[]\n  middlewares : Array Middleware := #[]\n\ndef Router.empty                                          : Router\ndef Router.addRoute      (route : Route) (self : Router)     : Router\ndef Router.addRouter     (self : Router) (pre : String) (sub : Router) : Router\ndef Router.addMiddleware (middleware : Middleware) (r : Router) : Router\ndef Router.toRouteTrie   (self : Router)                  : RouteTrie\ndef Router.serve         (self : Router) (addr : Net.SocketAddress)\n                         (config : Config := {}) (backlog : UInt32 := 1024) : Async Server\n```\n\n### 6.1 Route trie\n\nThe `RouteTrie` is a segment-based dispatch tree. Each node has:\n\n| Field      | Type                           | Purpose                                        |\n| ---------- | ------------------------------ | ---------------------------------------------- |\n| `handlers` | `HashMap Method HandlerFn`     | Handlers at this node (leaf or prefix)         |\n| `literals` | `HashMap.Raw String RouteTrie` | Exact segment matches (`/todos`)               |\n| `param`    | `Option (String × RouteTrie)`  | Single-segment capture (`{id}`)                |\n| `wildcard` | `Option (String × RouteTrie)`  | Remainder capture (`{*rest}`), lowest priority |\n\n**Lookup.** Given a method and a list of path segments, `RouteTrie.lookup` walks the\ntrie from the root. At each node, it tries literal match, then param match, then\nwildcard — taking the first match found. Returns the captured params (a `List (String × String)`)\nand the handler, or `none` if no route matches.\n\n### 6.2 Adding routes\n\n`addRoute` appends a route to the router's route list. At `toRouteTrie` time each\nroute is inserted into the trie with its route-level middlewares **pre-composed**\nonto the handler (using `foldl`, so the last route-level middleware added wraps\noutermost), wrapped by the enclosing router's middlewares.\n\n```lean\nRouter.empty\n  |>.addRoute listTasks\n  |>.addRoute addTask     -- for an identical method+pattern, the first one wins\n```\n\nRoute-level middleware is added to `Route` before registration:\n\n```lean\ndef rateLimited : Middleware := ...\nRouter.empty\n  |>.addRoute (myRoute.addMiddleware rateLimited)\n```\n\n### 6.3 Sub-router mounting\n\n`addRouter` records `(pre, sub)` in the router's sub-router list. At `toRouteTrie`\ntime:\n\n1. `sub` is recursively compiled into its own trie (composing `sub`'s middlewares\n   onto its handlers).\n2. `pre` is parsed into a list of `Segment` values.\n3. Every handler of the compiled sub-trie is re-inserted into the parent trie with\n   the `pre` segments prepended, wrapped by the parent's middlewares.\n\nThe sub-router's middlewares only apply to routes originally from that sub-router.\nThe compiled trie is flat — dispatch is a single trie walk.\n\n```lean\ndef apiV1 : Router := Router.empty\n  |>.addRoute listItems\n  |>.addRoute createItem\n  |>.addMiddleware apiAuth       -- applies to listItems and createItem\n\ndef root : Router := Router.empty\n  |>.addRouter \"/api/v1\" apiV1   -- mounted under /api/v1 when compiled\n  |>.addMiddleware requestLogger -- applies to ALL routes\n```\n\n### 6.4 Dispatch\n\nDispatch is implemented by `RouteTrie`, which implements\n`Std.Http.Server.Handler`. On each incoming request:\n\n1. The path is split into decoded segments.\n2. `RouteTrie.lookup` walks the trie: literal > param > wildcard priority.\n3. On match, captured parameter names and values are injected into the request's\n   extension map as `RouteParams`. Extractors read them back via `FromRequestParts`.\n4. The stored handler — already wrapped with route, sub-router and router\n   middlewares at compile time — is called with the enriched request.\n\n```mermaid\nsequenceDiagram\n  participant S as Server\n  participant T as RouteTrie\n  participant M as Middlewares\n  participant H as Handler\n  S->>T: onRequest\n  T->>T: lookup(method, pathSegs)\n  T-->>T: inject RouteParams\n  T->>M: call pre-composed chain\n  M->>H: next req\n  H-->>M: response\n  M-->>T: return\n  T-->>S: response\n```\n\nIf no route matches, the trie returns `404 Not Found`.\n\n### 6.5 Server integration\n\n`Router.serve` compiles the router with `toRouteTrie` and hands the trie to\n`Std.Http.Server.serve` (`RouteTrie` implements `Std.Http.Server.Handler`):\n\n```lean\ndef main : IO Unit := Async.block do\n  let addr : Net.SocketAddress := .v4 ⟨.ofParts 127 0 0 1, 8080⟩\n  let server ← router.serve addr\n  server.waitShutdown\n```\n\n---\n\n## 7. Reference\n\n### 7.1 Misc\n\n#### Extended header names\n\nAdditional `Std.Http.Header.Name` constants beyond Std's built-in set:\n\n| Constant             | Value                 |\n| -------------------- | --------------------- |\n| `contentDisposition` | `content-disposition` |\n| `acceptRanges`       | `accept-ranges`       |\n| `contentRange`       | `content-range`       |\n| `range`              | `range`               |\n| `wwwAuthenticate`    | `www-authenticate`    |\n| `cacheControl`       | `cache-control`       |\n| `etag`               | `etag`                |\n| `ifNoneMatch`        | `if-none-match`       |\n| `lastModified`       | `last-modified`       |\n| `ifModifiedSince`    | `if-modified-since`   |\n\n#### MIME type constants\n\n| Constant                   | Value                               |\n| -------------------------- | ----------------------------------- |\n| `MimeType.octetStream`     | `application/octet-stream`          |\n| `MimeType.textPlain`       | `text/plain`                        |\n| `MimeType.textHtml`        | `text/html`                         |\n| `MimeType.textCss`         | `text/css`                          |\n| `MimeType.textJavascript`  | `text/javascript`                   |\n| `MimeType.imagePng`        | `image/png`                         |\n| `MimeType.imageJpeg`       | `image/jpeg`                        |\n| `MimeType.imageSvg`        | `image/svg+xml`                     |\n| `MimeType.imageWebp`       | `image/webp`                        |\n| `MimeType.videoMp4`        | `video/mp4`                         |\n| `MimeType.videoWebm`       | `video/webm`                        |\n| `MimeType.audioMpeg`       | `audio/mpeg`                        |\n| `MimeType.applicationJson` | `application/json`                  |\n| `MimeType.applicationPdf`  | `application/pdf`                   |\n| `MimeType.applicationZip`  | `application/zip`                   |\n| `MimeType.formUrlEncoded`  | `application/x-www-form-urlencoded` |\n| `MimeType.multipartForm`   | `multipart/form-data`               |\n\n### 7.2 Examples\n\n| File                                                           | Description                                                                   |\n| -------------------------------------------------------------- | ----------------------------------------------------------------------------- |\n| [`Examples/Todos.lean`](./Examples/Todos.lean)                 | Full REST API: todos + comments, pagination, auth, sub-routers, catch-all     |\n| [`Examples/Upload.lean`](./Examples/Upload.lean)               | File uploads: `MultiPartForm` streaming, `Form` URL-encoded body              |\n| [`Examples/SumServer.lean`](./Examples/SumServer.lean)         | Sum types: `Json T ⊕ Form T` dispatches on `Content-Type`                     |\n| [`Examples/LeanPlay/Main.lean`](./Examples/LeanPlay/Main.lean) | Video browser: static file serving with `File`/`RangeFile`, custom middleware |\n| [`Examples/WhoAmI/WhoAmI.lean`](./Examples/WhoAmI/WhoAmI.lean) | Client IP: `RemoteAddr` extractor with a single-page frontend                 |\n\nRun an example:\n\n```bash\nlake build todos // or upload sumtest leanplay whoami\nlake exec todos\n```\n\n### 7.3 Requirements & installation\n\n- Lean **4.33.0** (pinned in `lean-toolchain`)\n- Lake\n\n```bash\ngit clone https://github.com/ecyrbe/leanio\ncd leanio\nlake build\nlake test\n```\n\n---\n\n## License\n\nMIT. See [`LICENSE`](./LICENSE).\n",1786977010416]