Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
f11bd09
feat: server basics
algebraic-dev Jan 25, 2026
bc21289
feat: http docs
algebraic-dev Jan 25, 2026
c4eab3b
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 10, 2026
4a9de70
feat: new body
algebraic-dev Feb 10, 2026
25dac2e
fix: test
algebraic-dev Feb 10, 2026
0aeaa5e
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 10, 2026
202e6c5
fix: transport, add explicit close that is no-op for tp
algebraic-dev Feb 11, 2026
7928a95
tests: add more tests
algebraic-dev Feb 11, 2026
5a2ad22
fix: selectable.one can used to change register/unregister order caus…
algebraic-dev Feb 11, 2026
4e4702a
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 11, 2026
fb0e95d
fix: avoid gate errors
algebraic-dev Feb 11, 2026
6b6425e
fix: close mock bidirectionaly and fix test
algebraic-dev Feb 11, 2026
1b6cd45
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 11, 2026
04a17e8
fix: fail event should end everything
algebraic-dev Feb 11, 2026
d902c6a
fix: mock double close
algebraic-dev Feb 11, 2026
2b91589
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 11, 2026
157e122
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 11, 2026
b40ac55
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 11, 2026
d206f43
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 12, 2026
73af014
fix: documentation
algebraic-dev Feb 12, 2026
26c0e4d
feat: date header
algebraic-dev Feb 13, 2026
8d6ff0d
feat: handler
algebraic-dev Feb 13, 2026
5b4498a
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 13, 2026
7ff0e6f
feat: 100-continue
algebraic-dev Feb 13, 2026
0704f87
fix: tests
algebraic-dev Feb 13, 2026
4906e14
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 13, 2026
2a04014
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 13, 2026
6dc19ef
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 13, 2026
1180572
fix: test
algebraic-dev Feb 13, 2026
5b14935
feat: body channel should close on completion
algebraic-dev Feb 13, 2026
991a27b
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 13, 2026
3561d58
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 13, 2026
8017d39
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 13, 2026
af58b4f
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 14, 2026
b6705cc
fix: server
algebraic-dev Feb 14, 2026
cdcb9db
Merge branch 'sofia/async-http-h1' of https://github.com/leanprover/l…
algebraic-dev Feb 14, 2026
e8ff308
fix: handler
algebraic-dev Feb 16, 2026
c50fca3
fix: comments
algebraic-dev Feb 16, 2026
fa8d76f
fix: frameCancellation with error
algebraic-dev Feb 16, 2026
7ee3756
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 17, 2026
17a2c9e
feat: server
algebraic-dev Feb 17, 2026
60d0b7c
feat: server
algebraic-dev Feb 17, 2026
7f29fd0
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 17, 2026
70c0a90
fix: api
algebraic-dev Feb 17, 2026
5193b73
fix: typos
algebraic-dev Feb 17, 2026
1826257
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 17, 2026
07a05a3
fix: 100-expect
algebraic-dev Feb 17, 2026
b7d4e12
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 17, 2026
1cbd056
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
0c5d25a
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
bed5d85
feat: incomplete chunks
algebraic-dev Feb 18, 2026
45747bd
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
b6fdd8a
feat: failure gate
algebraic-dev Feb 18, 2026
71420f6
feat: tests
algebraic-dev Feb 18, 2026
9bb429d
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
5925397
Merge branch 'sofia/async-http-body' into sofia/async-http-server
algebraic-dev Feb 18, 2026
9a8bc52
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
42b726c
fix: misleading comment
algebraic-dev Feb 18, 2026
892ab92
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
2b408d2
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
299b15c
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
1095ebb
fix: config
algebraic-dev Feb 18, 2026
c6a3ab0
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
f311c95
feat: unsuppoted method
algebraic-dev Feb 18, 2026
d1f090e
fix: test
algebraic-dev Feb 18, 2026
5368b13
fix: test
algebraic-dev Feb 18, 2026
2da4e1b
feat: connection lmit
algebraic-dev Feb 18, 2026
8d349cc
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
afd2f12
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 18, 2026
7dc97a0
fix: comments
algebraic-dev Feb 19, 2026
3d07f4f
fix: comments
algebraic-dev Feb 19, 2026
836cdf4
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 19, 2026
b40bc2e
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 19, 2026
330e1c5
fix: config
algebraic-dev Feb 19, 2026
b52bbc9
fix: rfc expected
algebraic-dev Feb 19, 2026
469f466
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
fffc2b5
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
4716725
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
d317c02
feat: body
algebraic-dev Feb 20, 2026
3c877f9
feat: body
algebraic-dev Feb 20, 2026
bfdfabd
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
2e1406b
fix: connection handler
algebraic-dev Feb 20, 2026
cf603cd
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
8cef903
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
11cc11b
fix: test
algebraic-dev Feb 20, 2026
cf51a32
fix: space sequence
algebraic-dev Feb 20, 2026
a182a66
feat: omit body
algebraic-dev Feb 20, 2026
b5610a4
feat: test
algebraic-dev Feb 20, 2026
32d42b5
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
5528f97
fix: dedup
algebraic-dev Feb 20, 2026
1d0e26e
tests: dedu
algebraic-dev Feb 20, 2026
9291e92
fix: commment
algebraic-dev Feb 20, 2026
02adf1f
fix: dedup
algebraic-dev Feb 20, 2026
04c73b6
test: fuzz
algebraic-dev Feb 20, 2026
7cf4194
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
5263c32
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
c9cf60f
fix: timeout on slow connections
algebraic-dev Feb 20, 2026
0e0578e
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
c7bcd4f
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 20, 2026
7a50344
feat: add header max config
algebraic-dev Feb 20, 2026
a6f42ab
feat: remove lenience
algebraic-dev Feb 20, 2026
abf3305
fix: move test from 200 to 400
algebraic-dev Feb 20, 2026
d0e884d
fix: config
algebraic-dev Feb 20, 2026
662bed5
tests: add
algebraic-dev Feb 21, 2026
682e2b9
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 21, 2026
11fd4c8
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 24, 2026
3a3620e
fix: tests
algebraic-dev Feb 24, 2026
fd96be3
feat: rfc compliance with some features
algebraic-dev Feb 24, 2026
6d1a0ec
fix: semaphore
algebraic-dev Feb 24, 2026
7160b92
fix: semaphore
algebraic-dev Feb 24, 2026
df80ac7
fix: semaphore
algebraic-dev Feb 25, 2026
91275b3
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 25, 2026
1cd2cba
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 25, 2026
f543206
fix: test
algebraic-dev Feb 25, 2026
cba7bfb
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 25, 2026
9c1054a
fix: slow attack
algebraic-dev Feb 25, 2026
c971d3f
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 25, 2026
81c3e50
fix: pull
algebraic-dev Feb 25, 2026
405d03a
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 25, 2026
625e1c9
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 26, 2026
67300c6
fix: tests
algebraic-dev Feb 26, 2026
c467175
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 26, 2026
e569c9e
feat: remove unacurate test
algebraic-dev Feb 27, 2026
5fd94a1
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 27, 2026
6fa6d2e
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 27, 2026
74ecbca
feat: tests
algebraic-dev Feb 28, 2026
4614def
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 28, 2026
0eb4a6e
fix: timeout and config
algebraic-dev Feb 28, 2026
c1b5b64
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 28, 2026
4306782
Merge branch 'sofia/async-http-uri' into sofia/async-http-server
algebraic-dev Feb 28, 2026
6dbb6b8
Merge branch 'sofia/async-http-h1' into sofia/async-http-server
algebraic-dev Feb 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/Std/Internal/Async/Select.lean
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public import Init.Data.Random
public import Std.Internal.Async.Basic
import Init.Data.ByteArray.Extra
import Init.Data.Array.Lemmas
public import Std.Sync.Mutex
public import Std.Sync.Barrier
import Init.Omega

public section
Expand Down Expand Up @@ -132,6 +134,8 @@ partial def Selectable.one (selectables : Array (Selectable α)) : Async α := d
let gen := mkStdGen seed
let selectables := shuffleIt selectables gen

let gate ← IO.Promise.new

for selectable in selectables do
if let some val ← selectable.selector.tryFn then
let result ← selectable.cont val
Expand All @@ -141,6 +145,9 @@ partial def Selectable.one (selectables : Array (Selectable α)) : Async α := d
let promise ← IO.Promise.new

for selectable in selectables do
if ← finished.get then
break

let waiterPromise ← IO.Promise.new
let waiter := Waiter.mk finished waiterPromise
selectable.selector.registerFn waiter
Expand All @@ -157,18 +164,20 @@ partial def Selectable.one (selectables : Array (Selectable α)) : Async α := d
let async : Async _ :=
try
let res ← IO.ofExcept res
discard <| await gate.result?

for selectable in selectables do
selectable.selector.unregisterFn

let contRes ← selectable.cont res
promise.resolve (.ok contRes)
promise.resolve (.ok (← selectable.cont res))
catch e =>
promise.resolve (.error e)

async.toBaseIO

Async.ofPromise (pure promise)
gate.resolve ()
let result ← Async.ofPromise (pure promise)
return result

/--
Performs fair and data-loss free non-blocking multiplexing on the `Selectable`s in `selectables`.
Expand Down Expand Up @@ -224,13 +233,16 @@ def Selectable.combine (selectables : Array (Selectable α)) : IO (Selector α)
let derivedWaiter := Waiter.mk waiter.finished waiterPromise
selectable.selector.registerFn derivedWaiter

let barrier ← IO.Promise.new

discard <| IO.bindTask (t := waiterPromise.result?) fun res? => do
match res? with
| none => return (Task.pure (.ok ()))
| some res =>
let async : Async _ := do
let mainPromise := waiter.promise

await barrier
for selectable in selectables do
selectable.selector.unregisterFn

Expand Down
187 changes: 185 additions & 2 deletions src/Std/Internal/Http.lean
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,188 @@ Authors: Sofia Rodrigues
module

prelude
public import Std.Internal.Http.Data
public import Std.Internal.Http.Protocol.H1
public import Std.Internal.Http.Server

public section

/-!
# HTTP Library

A low-level HTTP/1.1 server implementation for Lean. This library provides a pure,
sans-I/O protocol implementation that can be used with the `Async` library or with
custom connection handlers.

## Overview

This module provides a complete HTTP/1.1 server implementation with support for:

- Request/response handling with directional streaming bodies
- Keep-alive connections
- Chunked transfer encoding
- Header validation and management
- Configurable timeouts and limits

**Sans I/O Architecture**: The core protocol logic doesn't perform any actual I/O itself -
it just defines how data should be processed. This separation allows the protocol implementation
to remain pure and testable, while different transports (TCP sockets, mock clients) handle
the actual reading and writing of bytes.

## Quick Start

The main entry point is `Server.serve`, which starts an HTTP/1.1 server. Implement the
`Server.Handler` type class to define how the server handles requests, errors, and
`Expect: 100-continue` headers:

```lean
import Std.Internal.Http

open Std Internal IO Async
open Std Http Server

structure MyHandler

instance : Handler MyHandler where
onRequest _ req := do
Response.ok |>.text "Hello, World!"

def main : IO Unit := Async.block do
let addr : Net.SocketAddress := .v4 ⟨.ofParts 127 0 0 1, 8080⟩
let server ← Server.serve addr MyHandler.mk
server.waitShutdown
```

## Working with Requests

Incoming requests are represented by `Request Body.Incoming`, which bundles the request
line, parsed headers, and a lazily-consumed body. Headers are available immediately,
while the body can be streamed or collected on demand, allowing handlers to process both
small and large payloads efficiently.

### Reading Headers

```lean
def handler (req : Request Body.Incoming) : ContextAsync (Response Body.Outgoing) := do
-- Access request method and URI
let method := req.head.method -- Method.get, Method.post, etc.
let uri := req.head.uri -- RequestTarget

-- Read a specific header
if let some contentType := req.head.headers.get? (.mk "content-type") then
IO.println s!"Content-Type: {contentType}"

Response.ok |>.text "OK"
```

### URI Query Semantics

`RequestTarget.query` is parsed using form-style key/value conventions (`k=v&...`), and `+` is decoded as a
space in query components. If you need RFC 3986 opaque query handling, use the raw request target string
(`toString req.head.uri`) and parse it with custom logic.

### Reading the Request Body

The request body is exposed as `Body.Incoming`, which can be consumed incrementally or
collected into memory. The `readAll` method reads the entire body, with an optional size
limit to protect against unbounded payloads.

```lean
def handler (req : Request Body.Incoming) : ContextAsync (Response Body.Outgoing) := do
-- Collect entire body as a String
let bodyStr : String ← req.body.readAll

-- Or with a maximum size limit
let bodyStr : String ← req.body.readAll (maximumSize := some 1024)

Response.ok |>.text s!"Received: {bodyStr}"
```

## Building Responses

Responses are constructed using a builder API that starts from a status code and adds
headers and a body. Common helpers exist for text, HTML, JSON, and binary responses, while
still allowing full control over status codes and header values.

Response builders produce `Async (Response Body.Outgoing)`.

```lean
-- Text response
Response.ok |>.text "Hello!"

-- HTML response
Response.ok |>.html "<h1>Hello!</h1>"

-- JSON response
Response.ok |>.json "{\"key\": \"value\"}"

-- Binary response
Response.ok |>.bytes someByteArray

-- Custom status
Response.new |>.status .created |>.text "Resource created"

-- With custom headers
Response.ok
|>.header! "X-Custom-Header" "value"
|>.header! "Cache-Control" "no-cache"
|>.text "Response with headers"
```

### Streaming Responses

For large responses or server-sent events, use streaming:

```lean
def handler (req : Request Body.Incoming) : ContextAsync (Response Body.Outgoing) := do
Response.ok
|>.header! "Content-Type" "text/plain"
|>.stream fun stream => do
for i in [0:10] do
stream.send { data := s!"chunk {i}\n".toUTF8 }
Async.sleep 1000
stream.close
```

## Server Configuration

Configure server behavior with `Config`:

```lean
def config : Config := {
maxRequests := 10000000,
lingeringTimeout := 5000,
}

let server ← Server.serve addr MyHandler.mk config
```

## Handler Type Class

Implement `Server.Handler` to define how the server processes events. The class has three
methods, all with default implementations:

- `onRequest` — called for each incoming request; returns a response inside `ContextAsync`
- `onFailure` — called when an error occurs while processing a request
- `onContinue` — called when a request includes an `Expect: 100-continue` header; return
`true` to accept the body or `false` to reject it

```lean
structure MyHandler where
greeting : String

instance : Handler MyHandler where
onRequest self req := do
Response.ok |>.text self.greeting

onFailure self err := do
IO.eprintln s!"Error: {err}"
```

The handler methods operate in the following monads:

- `onRequest` uses `ContextAsync` — an asynchronous monad (`ReaderT CancellationContext Async`) that provides:
- Full access to `Async` operations (spawning tasks, sleeping, concurrent I/O)
- A `CancellationContext` tied to the client connection — when the client disconnects, the
context is cancelled, allowing your handler to detect this and stop work early
- `onFailure` uses `Async`
- `onContinue` uses `Async`
-/
15 changes: 6 additions & 9 deletions src/Std/Internal/Http/Protocol/H1.lean
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,9 @@ private def hasSingleAcceptedHostHeader (message : Message.Head .receiving) : Bo
| some ⟨host, _⟩ => authority.host == host -- && authority.port == port (Not so strict!)
| none => false
| _ =>
match parsed with
| some _ => true
| none => false
-- RFC 9110 §7.2: when the target URI has no authority (origin-form, asterisk-form),
-- a client MUST send Host with an empty field value, so an empty value is valid here.
hostValue.value.isEmpty ∨ parsed.isSome
| _ => false

/--
Expand Down Expand Up @@ -1205,6 +1205,7 @@ private def parseChunkSizeBody (machine : Machine dir) :
let (machine, result) := parseWith machine
(parseChunkSize machine.config)
(limit := some machine.config.maxChunkLineLength)

match result with
| some (size, ext) =>
if size > machine.config.maxChunkSize then
Expand All @@ -1224,10 +1225,8 @@ private def parseLastChunkBodyState (machine : Machine dir)
Machine dir × Option PulledChunk × Bool :=
let (machine, result) := parseWith machine (parseLastChunkBody machine.config) (limit := none)
match result with
| some _ =>
emitBodyChunk machine .complete true false ext .empty (closeBody := true)
| none =>
bodyNoProgress machine
| some _ => emitBodyChunk machine .complete true false ext .empty (closeBody := true)
| none => bodyNoProgress machine

/--
Consumes chunk-data bytes for the current chunk, handling complete and partial
Expand Down Expand Up @@ -1296,8 +1295,6 @@ mutual
/--
Parses one request start-line in server mode and initializes `reader.messageHead`
for header parsing.

Unsupported methods or versions are converted into protocol failures.
-/
private partial def processReceivingStartLine (machine : Machine .receiving) : Machine .receiving :=

Expand Down
Loading
Loading