[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"soWdqMTkyr":3},"# HTTP Client for Lean 4\n\n> **Note:** This project was vibe coded with the help of Claude Code and Cursor. No guarantees.\n\nA type-safe, composable HTTP/1.1 client library for Lean 4. Built on top of `curl` via FFI, providing a clean, idiomatic Lean interface for making HTTP requests.\n\n## Features\n\n- **Type Safety**: Leverages Lean's type system to prevent invalid HTTP constructs\n- **Pure API**: Separates pure HTTP logic from IO operations\n- **Composability**: Build complex requests from simple, composable parts\n- **Correct**: Follows HTTP/1.1 specification (RFC 7230-7235)\n- **Curl Transport**: Uses the battle-tested `curl` library under the hood\n- **Redirect Support**: Automatic HTTP redirect following (configurable)\n- **JSON Support**: Convenient builders for JSON requests\n- **Custom Headers**: Flexible header management with case-insensitive lookups\n\n## Installation\n\n### Prerequisites\n\n- **Lean 4** (v4.25.2 or later)\n- **curl** (usually pre-installed on macOS/Linux)\n  - macOS: `brew install curl` or use system curl\n  - Ubuntu/Debian: `sudo apt-get install curl`\n  - Fedora/RHEL: `sudo dnf install curl`\n\n### Add to Your Project\n\nAdd this to your `lakefile.lean`:\n\n```lean\nrequire http-client from git \"https://github.com/hargup/lean-http-client.git\"\n```\n\nThen run:\n\n```bash\nlake update\n```\n\n## Quick Start\n\n### Simple GET Request\n\n```lean\nimport HttpClient\n\ndef main : IO Unit := do\n  match ← HttpClient.get \"https://api.example.com/data\" with\n  | .ok res =>\n    IO.println s!\"Status: {res.status.code}\"\n    IO.println res.body\n  | .error err =>\n    IO.eprintln s!\"Error: {err}\"\n```\n\n### POST Request with JSON\n\n```lean\nimport HttpClient\n\ndef main : IO Unit := do\n  let json := \"\"\"{\"name\": \"John\", \"age\": 30}\"\"\"\n  match ← HttpClient.postJson \"https://api.example.com/users\" json with\n  | .ok res => IO.println res.body\n  | .error err => IO.eprintln s!\"Error: {err}\"\n```\n\n### Custom Client with Configuration\n\n```lean\nimport HttpClient\n\ndef main : IO Unit := do\n  let config : HttpClient.Config := {\n    followRedirects := true\n    maxRedirects := 5\n    timeout := 30  -- seconds\n  }\n  let client ← HttpClient.Client.new config\n\n  let url := HttpClient.Url.parse \"https://api.example.com/data\" |>.get!\n  let req := HttpClient.Request.get url\n                |>.withHeader \"Authorization\" \"Bearer token123\"\n\n  match ← client.send req with\n  | .ok res => IO.println res.body\n  | .error err => IO.eprintln s!\"Error: {err}\"\n```\n\n## Core API\n\n### Creating URLs\n\n```lean\n-- Parse from string\nlet url? := HttpClient.Url.parse \"https://example.com/api?key=value\"\n\n-- Create manually\nlet url := HttpClient.Url.http \"example.com\" \"/api\"\nlet url := HttpClient.Url.https \"example.com\" \"/api\"\n\n-- Modify URLs\nlet url := HttpClient.Url.http \"example.com\"\n  |>.withPort 8080\n  |>.withPath \"/api/v1\"\n  |>.withQuery \"key=value\"\n```\n\n### Building Requests\n\n```lean\nlet req := HttpClient.Request.get url\n  |>.withHeader \"User-Agent\" \"MyApp/1.0\"\n  |>.withHeader \"Accept\" \"application/json\"\n\nlet req := HttpClient.Request.post url\n  |>.withBody \"raw body content\"\n\nlet req := HttpClient.Request.post url\n  |>.withJson \"{\\\"key\\\": \\\"value\\\"}\"\n  -- Automatically sets Content-Type: application/json\n\nlet req := HttpClient.Request.put url\n  |>.withBody \"updated data\"\n\nlet req := HttpClient.Request.delete url\n```\n\n### Accessing Response Data\n\n```lean\nlet res : HttpClient.Response := ...\n\n-- Status information\nlet code : Nat := res.status.code\nlet reason : String := res.status.reason\nlet isSuccess := res.status.isSuccess  -- 2xx responses\n\n-- Headers\nlet contentType := res.headers.contentType\nlet contentLength := res.headers.contentLength\nlet customHeader := res.headers.get \"X-Custom-Header\"\n\n-- Body\nlet body : String := res.body\n```\n\n## Error Handling\n\nAll HTTP operations return `IO (HttpResult Response)` where `HttpResult` is `Except HttpError`.\n\n```lean\ninductive HttpError where\n  | parseError (msg : String)      -- URL or response parsing failed\n  | connectionError (msg : String) -- Network error\n  | timeoutError                   -- Request timeout\n  | protocolError (msg : String)   -- HTTP protocol violation\n  | systemError (msg : String)     -- curl or system error\n```\n\nPattern match on errors:\n\n```lean\nmatch ← HttpClient.get url with\n| .ok res => IO.println res.body\n| .error (.parseError msg) => IO.eprintln s!\"Parse error: {msg}\"\n| .error (.connectionError msg) => IO.eprintln s!\"Connection failed: {msg}\"\n| .error .timeoutError => IO.eprintln \"Request timed out\"\n| .error err => IO.eprintln s!\"Error: {err}\"\n```\n\n## Configuration\n\nThe `Config` structure allows customization:\n\n```lean\nstructure Config where\n  defaultHeaders : Headers := Headers.empty\n  followRedirects : Bool := true\n  maxRedirects : Nat := 5\n  timeout : Nat := 30  -- seconds\n```\n\nExample:\n\n```lean\nlet config : HttpClient.Config := {\n  defaultHeaders := [(\"User-Agent\", \"MyApp/1.0\")]\n  followRedirects := false\n  timeout := 60\n}\n\nlet client ← HttpClient.Client.new config\n```\n\n## Advanced Usage\n\n### Custom Headers\n\n```lean\nlet req := HttpClient.Request.get url\n  |>.withHeader \"Authorization\" \"Bearer token\"\n  |>.withHeader \"X-API-Key\" \"secret\"\n\n-- Access headers\nlet authHeader := req.headers.get \"Authorization\"\n\n-- Case-insensitive header lookup\nlet contentType := req.headers.get \"content-type\"  -- matches \"Content-Type\"\n```\n\n### Testing with Mock Transport\n\nFor testing without network access, you can create a mock transport:\n\n```lean\nlet mockTransport : HttpClient.Transport := {\n  send := fun req _ => do\n    if req.url.host == \"mock.local\" then\n      return .ok (HttpClient.Response.ok \"Mock response\")\n    else\n      return .error (.connectionError \"Unknown host\")\n}\n\nlet client : HttpClient.Client := {\n  config := {}\n  transport := mockTransport\n}\n\nlet res ← client.send (HttpClient.Request.get testUrl)\n```\n\n## Module Structure\n\n```\nHttpClient/\n├── Basic.lean          -- Core types (Method, Status, Headers)\n├── Url.lean            -- URL parsing and representation\n├── Request.lean        -- HTTP Request type and builder\n├── Response.lean       -- HTTP Response type and parser\n├── Message.lean        -- HTTP message serialization\n├── Connection.lean     -- Transport interface and implementations\n├── Client.lean         -- High-level Client struct and logic\n└── HttpClient.lean     -- Main module (re-exports)\n```\n\n## Implementation Details\n\n### Curl Transport\n\nThe default transport implementation uses `curl` via `IO.Process.spawn`:\n- Maps configuration options to curl flags (`--max-time`, `--location`, etc.)\n- Parses curl's output to construct Response objects\n- Handles curl exit codes and maps them to `HttpError`\n\n### Why Curl?\n\nUsing curl via FFI provides:\n- **Battle-tested**: Curl is used by millions of applications\n- **Feature-complete**: HTTP/2 support, advanced TLS options, proxy support\n- **Portable**: Works across all major platforms\n- **Minimal Dependencies**: No need for large Lean networking libraries\n\n## Examples\n\nSee the `Example.lean` file for a working example:\n\n```bash\nlake build && .lake/build/bin/http-client\n```\n\nThis fetches from `http://example.com` and displays the response status, headers, and body.\n\n## Testing\n\nThe project includes comprehensive unit tests (Tests.lean):\n\n```lean\n-- URL parsing\n-- Request building\n-- Response parsing\n-- Mock transport verification\n```\n\nRun tests with:\n\n```bash\nlake build Tests\n```\n\n## Performance Characteristics\n\n- **Startup**: ~50-100ms (process spawn overhead for curl)\n- **Request**: Depends on network latency (curl's performance)\n- **Memory**: Minimal - responses stored entirely in memory as strings\n\nFor high-performance scenarios or streaming large files, consider native socket implementations (future work).\n\n## Limitations\n\n- **No HTTP/2 or HTTP/3**: Uses HTTP/1.1 only\n- **In-memory only**: All request and response bodies are strings in memory\n- **No connection pooling**: Each request spawns a new curl process\n- **No streaming**: Cannot process large files without loading entirely into memory\n\n## Contributing\n\nContributions welcome! Areas for improvement:\n- Native socket implementation (no curl dependency)\n- HTTP/2 support\n- Streaming request/response bodies\n- Connection pooling\n- Cookie jar support\n\n## License\n\nMIT License. See LICENSE file for details.\n\n## Related Resources\n\n- [Curl Documentation](https://curl.se/docs/)\n- [HTTP/1.1 Specification (RFC 7230-7235)](https://tools.ietf.org/html/rfc7230)\n- [Lean 4 Documentation](https://lean-lang.org/lean4/doc/)\n",1777138845049]