ocaml http/1, http/2 and websocket client and server library
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: update for v0.2.0 three-layer architecture

- Add Router.scope, compile_scopes, Route.plug to routing.md
- Add Pipeline module documentation to plug-system.md
- Update request-lifecycle.md with three-layer diagram
- Replace Endpoint.start with Endpoint.to_handler + Server.run
- Add scope-based auth examples to authentication.md
- Add per-route plug examples to rate-limiting.md

All examples now use the new API pattern:
let handler = Endpoint.to_handler endpoint in
Server.run ~sw ~net ~config handler

+348 -136
+81 -18
docs/guides/plug-system.md
··· 48 48 |> Endpoint.router routes 49 49 ``` 50 50 51 + ## Pipeline Module 52 + 53 + For Phoenix-style middleware composition, use `Pipeline` to create reusable plug collections: 54 + 55 + ```ocaml 56 + let browser = Pipeline.create [ 57 + Plug.Compress.create (); 58 + Plug.Session.create ~store (); 59 + ] 60 + 61 + let api = Pipeline.create [ 62 + Plug.Cors.create (); 63 + Plug.Rate_limit.create ~clock ~requests:100 ~per:60.0 ~key:get_ip; 64 + ] 65 + 66 + let auth = Pipeline.create [ 67 + require_authenticated; 68 + ] 69 + ``` 70 + 71 + ### Pipeline Functions 72 + 73 + | Function | Description | 74 + |----------|-------------| 75 + | `Pipeline.create plugs` | Create pipeline from plug list | 76 + | `Pipeline.empty` | Empty pipeline (no-op) | 77 + | `Pipeline.plug pipeline p` | Add plug to end | 78 + | `Pipeline.plug_first pipeline p` | Add plug to beginning | 79 + | `Pipeline.compose p1 p2` | Combine two pipelines | 80 + | `Pipeline.apply pipeline handler` | Apply pipeline to handler | 81 + 82 + ### Using Pipelines with Router Scopes 83 + 84 + Pipelines are designed to work with `Router.scope`: 85 + 86 + ```ocaml 87 + let routes = Router.compile_scopes [ 88 + Router.scope "/" ~through:browser [ 89 + Router.Route.get "/" Handlers.index; 90 + Router.Route.get "/about" Handlers.about; 91 + ]; 92 + Router.scope "/api" ~through:api [ 93 + Router.Route.get "/users" Handlers.list_users; 94 + ]; 95 + Router.scope "/admin" ~through:auth [ 96 + Router.Route.get "/dashboard" Handlers.dashboard; 97 + ]; 98 + ] 99 + ``` 100 + 51 101 ## Built-in Plugs 52 102 53 103 HCS includes these plugs: ··· 167 217 Return early without calling the handler: 168 218 169 219 ```ocaml 170 - let auth_required : Plug.t = fun handler req -> 171 - match List.assoc_opt "authorization" req.Server.headers with 220 + let require_auth : Plug.t = fun handler req -> 221 + match Plug.Session.get "user_id" with 172 222 | Some _ -> handler req 173 - | None -> Server.respond ~status:`Unauthorized "Unauthorized" 223 + | None -> Response.redirect "/login" 174 224 ``` 175 225 176 226 ### Plug with Configuration ··· 228 278 229 279 let log_fn _level msg = print_endline msg 230 280 231 - let my_handler _req = 232 - Server.respond_json {|{"status":"ok"}|} 281 + let require_auth : Plug.t = fun handler req -> 282 + match Plug.Session.get "user_id" with 283 + | Some _ -> handler req 284 + | None -> Response.redirect "/login" 233 285 234 286 let () = 235 287 Eio_main.run @@ fun env -> 236 288 let clock = Eio.Stdenv.clock env in 289 + let net = Eio.Stdenv.net env in 237 290 let store = Plug.Session.Memory_store.create () in 238 291 239 - let pipeline = 240 - Plug.Recover.create (fun _ -> 241 - Server.respond ~status:`Internal_server_error "Error") 242 - @> Plug.Logger.create ~clock log_fn 243 - @> Plug.Compress.create () 244 - @> Plug.Timeout.create ~clock 30.0 245 - @> Plug.Session.create ~store () 246 - @> Plug.Cors.create () 247 - @> Plug.identity 248 - in 292 + let browser = Pipeline.create [ 293 + Plug.Compress.create (); 294 + Plug.Session.create ~store (); 295 + ] in 296 + 297 + let auth = Pipeline.create [ require_auth ] in 249 298 250 - let handler = Plug.apply pipeline my_handler in 299 + let routes = Router.compile_scopes [ 300 + Router.scope "/" ~through:browser [ 301 + Router.Route.get "/" (fun _ _ -> Server.respond_html "<h1>Home</h1>"); 302 + ]; 303 + Router.scope "/admin" ~through:auth [ 304 + Router.Route.get "/" (fun _ _ -> Server.respond_html "<h1>Admin</h1>"); 305 + ]; 306 + ] in 251 307 308 + let endpoint = 309 + Endpoint.create Endpoint.default_config 310 + |> Endpoint.plug (Plug.Recover.create (fun _ -> 311 + Server.respond ~status:`Internal_server_error "Error")) 312 + |> Endpoint.plug (Plug.Logger.create ~clock log_fn) 313 + |> Endpoint.router routes 314 + in 315 + let handler = Endpoint.to_handler endpoint in 252 316 Eio.Switch.run @@ fun sw -> 253 - let net = Eio.Stdenv.net env in 254 317 Server.run ~sw ~net ~config:Server.default_config handler 255 318 ``` 256 319 257 320 ## Next Steps 258 321 259 - - [Plugs Reference](../plugs/reference.md) - All built-in plugs documented 322 + - [Routing](routing.md) - Route scopes and per-route plugs 260 323 - [Writing Plugs](../plugs/writing-plugs.md) - Advanced plug patterns
+96 -42
docs/guides/request-lifecycle.md
··· 7 7 When a request arrives, it passes through these stages: 8 8 9 9 ``` 10 - Connection → Protocol Detection → Plug Pipeline → Router → Handler → Response 10 + Connection → Protocol Detection → Endpoint Plugs → Router (Scope Plugs) → Route Plugs → Handler → Response 11 11 ``` 12 12 13 13 Each stage transforms or routes the request until a response is generated. ··· 39 39 40 40 The `target` contains the raw request target. For `/users?id=5`, the target is `/users?id=5`. 41 41 42 - ## Plug Pipeline 42 + ## Three-Layer Plug Architecture 43 + 44 + HCS uses a Phoenix-style three-layer plug architecture: 45 + 46 + ``` 47 + Request 48 + 49 + ┌─────────────────────────────────────────┐ 50 + │ Endpoint Plugs (global) │ 51 + │ - Logger, Compress, Session, CSRF │ 52 + └─────────────────────────────────────────┘ 53 + 54 + ┌─────────────────────────────────────────┐ 55 + │ Router Scope Plugs (per scope) │ 56 + │ - Auth, Rate Limit, Negotiate │ 57 + └─────────────────────────────────────────┘ 58 + 59 + ┌─────────────────────────────────────────┐ 60 + │ Route Plugs (per route) │ 61 + │ - Strict rate limit, specific auth │ 62 + └─────────────────────────────────────────┘ 63 + 64 + Handler 65 + 66 + Response (back through plugs in reverse) 67 + ``` 43 68 44 - Plugs wrap the handler, executing in order for requests and reverse order for responses: 69 + ### Layer 1: Endpoint Plugs 70 + 71 + Global plugs that run for every request: 45 72 46 73 ```ocaml 47 74 let endpoint = 48 75 Endpoint.create config 49 - |> Endpoint.plug (Plug.Logger.create ~clock log_fn) (* 1st: runs first *) 50 - |> Endpoint.plug (Plug.Compress.create ()) (* 2nd *) 51 - |> Endpoint.plug (Plug.Session.create ~store ()) (* 3rd *) 76 + |> Endpoint.plug (Plug.Logger.create ~clock log_fn) 77 + |> Endpoint.plug (Plug.Compress.create ()) 78 + |> Endpoint.plug (Plug.Session.create ~store ()) 52 79 |> Endpoint.router routes 53 80 ``` 54 81 55 - Request flow: 56 - ``` 57 - Request → Logger → Compress → Session → Router → Handler 58 - ``` 82 + ### Layer 2: Scope Plugs 83 + 84 + Plugs that run for routes within a scope: 59 85 60 - Response flow: 61 - ``` 62 - Handler → Session → Compress → Logger → Response 86 + ```ocaml 87 + let auth = Pipeline.create [ require_authenticated ] 88 + let api = Pipeline.create [ Plug.Cors.create () ] 89 + 90 + let routes = Router.compile_scopes [ 91 + Router.scope "/api" ~through:api [ ... ]; 92 + Router.scope "/admin" ~through:auth [ ... ]; 93 + ] 63 94 ``` 64 95 65 - Each plug can: 66 - - Modify the request before passing it on 67 - - Short-circuit by returning a response directly 68 - - Modify the response after the handler runs 96 + ### Layer 3: Route Plugs 97 + 98 + Plugs for individual routes: 99 + 100 + ```ocaml 101 + Router.Route.post "/login" Handlers.login 102 + |> Router.Route.plug strict_rate_limit 103 + ``` 69 104 70 105 ## Router 71 106 72 - The router maps method + path to handlers: 107 + The router maps method + path to handlers with their associated plugs: 73 108 74 109 ```ocaml 75 - let routes = Router.compile [ 76 - Router.Route.get "/" Handlers.index; 77 - Router.Route.get "/users/:id" Handlers.show_user; 78 - Router.Route.post "/users" Handlers.create_user; 110 + let routes = Router.compile_scopes [ 111 + Router.scope "/" ~through:browser [ 112 + Router.Route.get "/" Handlers.index; 113 + Router.Route.get "/users/:id" Handlers.show_user; 114 + ]; 115 + Router.scope "/api" ~through:api [ 116 + Router.Route.post "/users" Handlers.create_user; 117 + ]; 79 118 ] 80 119 ``` 81 120 ··· 117 156 118 157 ## Complete Flow Example 119 158 120 - Here's a complete endpoint showing the full lifecycle: 159 + Here's a complete example showing the full lifecycle: 121 160 122 161 ```ocaml 162 + open Hcs 163 + 123 164 let log_fn _level msg = print_endline msg 124 165 125 - let routes = Router.compile [ 126 - Router.Route.get "/" (fun _params _req -> 127 - Server.respond_html "<h1>Home</h1>"); 128 - Router.Route.get "/api/status" (fun _params _req -> 129 - Server.respond_json {|{"status":"ok"}|}); 130 - ] 166 + let require_auth : Plug.t = fun handler req -> 167 + match Plug.Session.get "user_id" with 168 + | Some _ -> handler req 169 + | None -> Response.redirect "/login" 131 170 132 171 let () = 133 172 Eio_main.run @@ fun env -> 134 173 let clock = Eio.Stdenv.clock env in 174 + let net = Eio.Stdenv.net env in 135 175 let store = Plug.Session.Memory_store.create () in 136 176 177 + let browser = Pipeline.create [ Plug.Compress.create () ] in 178 + let auth = Pipeline.create [ require_auth ] in 179 + 180 + let routes = Router.compile_scopes [ 181 + Router.scope "/" ~through:browser [ 182 + Router.Route.get "/" (fun _params _req -> 183 + Server.respond_html "<h1>Home</h1>"); 184 + ]; 185 + Router.scope "/api" ~through:auth [ 186 + Router.Route.get "/status" (fun _params _req -> 187 + Server.respond_json {|{"status":"ok"}|}); 188 + ]; 189 + ] in 190 + 137 191 let endpoint = 138 - Endpoint.create { Endpoint.default_config with port = 8080 } 192 + Endpoint.create Endpoint.default_config 139 193 |> Endpoint.plug (Plug.Logger.create ~clock log_fn) 140 - |> Endpoint.plug (Plug.Compress.create ()) 141 194 |> Endpoint.plug (Plug.Session.create ~store ()) 142 195 |> Endpoint.router routes 143 196 in 144 - 145 - Endpoint.start endpoint ~env 197 + let handler = Endpoint.to_handler endpoint in 198 + Eio.Switch.run @@ fun sw -> 199 + Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 146 200 ``` 147 201 148 202 When a request for `GET /api/status` arrives: 149 203 150 204 1. Connection accepted, protocol detected (HTTP/1.1) 151 205 2. Request parsed: `{ meth = `GET; target = "/api/status"; ... }` 152 - 3. Logger plug: logs request start 153 - 4. Compress plug: passes through (no action on request) 154 - 5. Session plug: loads session from cookie 155 - 6. Router: matches `/api/status` → handler 156 - 7. Handler: returns `{ status = `OK; body = Body_string {|{"status":"ok"}|}; ... }` 157 - 8. Session plug: no changes to save 158 - 9. Compress plug: gzips body if client accepts 159 - 10. Logger plug: logs response 160 - 11. Response sent to client 206 + 3. **Endpoint plugs**: 207 + - Logger: logs request start 208 + - Session: loads session from cookie 209 + 4. Router: matches `/api/status` → auth scope 210 + 5. **Scope plugs**: 211 + - require_auth: checks session, passes through 212 + 6. Handler: returns `{ status = `OK; body = {|{"status":"ok"}|}; ... }` 213 + 7. Response flows back through plugs (reverse order) 214 + 8. Response sent to client 161 215 162 216 ## Health Checks 163 217
+96 -54
docs/guides/routing.md
··· 78 78 79 79 `/files/docs/readme.md` matches with `params = [("*", "docs/readme.md")]`. 80 80 81 + ## Route Scopes (Phoenix-style) 82 + 83 + Group routes with shared middleware using `Router.scope`: 84 + 85 + ```ocaml 86 + let browser = Pipeline.create [ Plug.Compress.create () ] 87 + let api = Pipeline.create [ Plug.Cors.create () ] 88 + let auth = Pipeline.create [ require_authenticated ] 89 + 90 + let routes = Router.compile_scopes [ 91 + Router.scope "/" ~through:browser [ 92 + Router.Route.get "/" Handlers.index; 93 + Router.Route.get "/about" Handlers.about; 94 + ]; 95 + Router.scope "/api" ~through:api [ 96 + Router.Route.get "/users" Handlers.list_users; 97 + Router.Route.get "/users/:id" Handlers.show_user; 98 + ]; 99 + Router.scope "/admin" ~through:auth [ 100 + Router.Route.get "/dashboard" Handlers.dashboard; 101 + Router.Route.post "/settings" Handlers.update_settings; 102 + ]; 103 + ] 104 + ``` 105 + 106 + Scopes provide: 107 + - **Path prefixing**: Routes in `/api` scope get `/api` prefix 108 + - **Shared middleware**: All routes in scope run through the pipeline 109 + - **Organization**: Group related routes together 110 + 111 + ## Per-Route Plugs 112 + 113 + Apply middleware to individual routes with `Router.Route.plug`: 114 + 115 + ```ocaml 116 + let strict_rate_limit = Plug.Rate_limit.create ~clock ~requests:5 ~per:60.0 117 + ~key:(fun _ -> "global") 118 + 119 + let routes = Router.compile_scopes [ 120 + Router.scope "/" ~through:browser [ 121 + Router.Route.get "/" Handlers.index; 122 + Router.Route.post "/login" Handlers.login 123 + |> Router.Route.plug strict_rate_limit; 124 + ]; 125 + ] 126 + ``` 127 + 128 + Per-route plugs run after scope plugs but before the handler. 129 + 130 + ## Combining Scopes and Per-Route Plugs 131 + 132 + ```ocaml 133 + let negotiate = Plug.Negotiate.create ~formats:[Json; Html] () 134 + let auth = Pipeline.create [ require_authenticated ] 135 + let submit_limit = Plug.Rate_limit.create ~clock ~requests:10 ~per:3600.0 136 + ~key:(fun _ -> Plug.Session.get "user_id" |> Option.value ~default:"anon") 137 + 138 + let routes = Router.compile_scopes [ 139 + Router.scope "/" ~through:Pipeline.empty [ 140 + Router.Route.get "/login" Handlers.login_form; 141 + Router.Route.post "/login" Handlers.login_submit; 142 + ]; 143 + Router.scope "/" ~through:auth [ 144 + Router.Route.get "/submit" Handlers.submit_form; 145 + Router.Route.post "/links" Handlers.create_link 146 + |> Router.Route.plug submit_limit 147 + |> Router.Route.plug negotiate; 148 + ]; 149 + ] 150 + ``` 151 + 81 152 ## Handler Signature 82 153 83 154 Route handlers receive params and request: ··· 122 193 Response.json (Printf.sprintf {|{"q":"%s","page":%d,"limit":%d}|} q page limit) 123 194 ``` 124 195 125 - The API supports piping with `|>`: 126 - 127 - ```ocaml 128 - let handler _params req = 129 - let name = req |> Request.query_or ~default:"world" @@ "name" in 130 - Response.html (Printf.sprintf "<h1>Hello, %s!</h1>" name) 131 - ``` 132 - 133 196 Available query helpers: 134 197 135 198 | Function | Returns | Description | ··· 140 203 | `Request.query_int req "key"` | `int option` | Parse as int | 141 204 | `Request.query_int_or ~default req "key"` | `int` | Parse as int with default | 142 205 | `Request.query_bool req "key"` | `bool option` | Parse as bool | 143 - | `Request.query_bool_or ~default req "key"` | `bool` | Parse as bool with default | 144 206 | `Request.query_float req "key"` | `float option` | Parse as float | 145 - | `Request.query_float_or ~default req "key"` | `float` | Parse as float with default | 146 - | `Request.query_params req` | `(string * string) list` | All parameters | 147 - 148 - Bool parsing accepts: `true`, `1`, `yes`, `on` (and their negatives). 149 - 150 - All values are automatically URL-decoded. 151 207 152 208 ## Form Data 153 209 ··· 160 216 Response.json (Printf.sprintf {|{"name":"%s","email":"%s"}|} name email) 161 217 ``` 162 218 163 - | Function | Returns | Description | 164 - |----------|---------|-------------| 165 - | `Request.form_data req` | `(string * string) list` | All form fields | 166 - | `Request.form_field req "key"` | `string option` | Get field value | 167 - | `Request.form_field_or ~default req "key"` | `string` | Get with default | 168 - 169 - ## Using with Endpoint 219 + ## Using with Endpoint and Server 170 220 171 - Connect the router to an endpoint: 221 + Connect the router to an endpoint and run the server: 172 222 173 223 ```ocaml 174 224 let routes = Router.compile [ ··· 211 261 let routes = Router.compile (user_routes @ api_routes) 212 262 ``` 213 263 214 - ## Complete Example 264 + ## Complete Example with Scopes 215 265 216 266 ```ocaml 217 267 open Hcs 218 268 219 - let list_users _params _req = 220 - Server.respond_json {|[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]|} 269 + let require_auth : Plug.t = fun handler req -> 270 + match Plug.Session.get "user_id" with 271 + | Some _ -> handler req 272 + | None -> Response.redirect "/login" 221 273 222 - let show_user params _req = 223 - let id = Router.param_int_or "id" ~default:0 params in 224 - Server.respond_json (Printf.sprintf {|{"id":%d}|} id) 274 + let () = 275 + Eio_main.run @@ fun env -> 276 + let clock = Eio.Stdenv.clock env in 277 + let net = Eio.Stdenv.net env in 278 + let store = Plug.Session.Memory_store.create () in 225 279 226 - let create_user _params req = 227 - let body = req.Server.body in 228 - Server.respond ~status:`Created body 280 + let browser = Pipeline.create [ Plug.Compress.create () ] in 281 + let auth = Pipeline.create [ require_auth ] in 229 282 230 - let routes = Router.compile [ 231 - Router.Route.get "/" (fun _ _ -> Server.respond_html "<h1>API</h1>"); 232 - Router.Route.get "/users" list_users; 233 - Router.Route.get "/users/:id" show_user; 234 - Router.Route.post "/users" create_user; 235 - ] 283 + let routes = Router.compile_scopes [ 284 + Router.scope "/" ~through:browser [ 285 + Router.Route.get "/" (fun _ _ -> Server.respond_html "<h1>Home</h1>"); 286 + Router.Route.get "/login" (fun _ _ -> Server.respond_html "<form>...</form>"); 287 + ]; 288 + Router.scope "/dashboard" ~through:auth [ 289 + Router.Route.get "/" (fun _ _ -> Server.respond_html "<h1>Dashboard</h1>"); 290 + Router.Route.get "/settings" (fun _ _ -> Server.respond_html "<h1>Settings</h1>"); 291 + ]; 292 + ] in 236 293 237 - let () = 238 - Eio_main.run @@ fun env -> 239 - let net = Eio.Stdenv.net env in 240 294 let endpoint = 241 295 Endpoint.create Endpoint.default_config 296 + |> Endpoint.plug (Plug.Session.create ~store ()) 242 297 |> Endpoint.router routes 243 298 in 244 299 let handler = Endpoint.to_handler endpoint in 245 300 Eio.Switch.run @@ fun sw -> 246 301 Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 247 - ``` 248 - 249 - Test it: 250 - 251 - ```bash 252 - curl http://localhost:8080/users 253 - # [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}] 254 - 255 - curl http://localhost:8080/users/42 256 - # {"id":42} 257 - 258 - curl -X POST -d '{"name":"Charlie"}' http://localhost:8080/users 259 - # {"name":"Charlie"} 260 302 ``` 261 303 262 304 ## Next Steps
+5 -2
docs/real-time/pubsub.md
··· 157 157 158 158 let () = 159 159 Eio_main.run @@ fun env -> 160 + let net = Eio.Stdenv.net env in 160 161 let endpoint = 161 - Endpoint.create { Endpoint.default_config with port = 8080 } 162 + Endpoint.create Endpoint.default_config 162 163 |> Endpoint.router routes 163 164 in 164 - Endpoint.start endpoint ~env 165 + let handler = Endpoint.to_handler endpoint in 166 + Eio.Switch.run @@ fun sw -> 167 + Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 165 168 ``` 166 169 167 170 Test with:
+5 -2
docs/real-time/sse.md
··· 168 168 169 169 let () = 170 170 Eio_main.run @@ fun env -> 171 + let net = Eio.Stdenv.net env in 171 172 let endpoint = 172 - Endpoint.create { Endpoint.default_config with port = 8080 } 173 + Endpoint.create Endpoint.default_config 173 174 |> Endpoint.router routes 174 175 in 175 - Endpoint.start endpoint ~env 176 + let handler = Endpoint.to_handler endpoint in 177 + Eio.Switch.run @@ fun sw -> 178 + Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 176 179 ``` 177 180 178 181 ## SSE Event Format
+27 -7
docs/recipes/authentication.md
··· 70 70 let require_auth : Hcs.Plug.t = fun handler req -> 71 71 match Hcs.Plug.Session.get "user_id" with 72 72 | Some _ -> handler req 73 - | None -> 74 - Server.respond ~status:`Found 75 - ~headers:[("Location", "/login")] "" 73 + | None -> Response.redirect "/login" 74 + ``` 75 + 76 + ## Using Auth with Route Scopes (Phoenix-style) 76 77 77 - (* Apply to protected routes *) 78 - let protected_handler = Hcs.Plug.apply require_auth my_handler 78 + Group protected routes in a scope with the auth plug: 79 + 80 + ```ocaml 81 + let auth = Pipeline.create [ require_auth ] 82 + 83 + let routes = Router.compile_scopes [ 84 + Router.scope "/" ~through:Pipeline.empty [ 85 + Router.Route.get "/login" Handlers.login_form; 86 + Router.Route.post "/login" Handlers.login_submit; 87 + ]; 88 + Router.scope "/" ~through:auth [ 89 + Router.Route.get "/dashboard" Handlers.dashboard; 90 + Router.Route.get "/settings" Handlers.settings; 91 + Router.Route.post "/logout" Handlers.logout; 92 + ]; 93 + ] 79 94 ``` 95 + 96 + All routes in the auth scope automatically require authentication. 80 97 81 98 ## CSRF Protection 82 99 ··· 194 211 195 212 let () = 196 213 Eio_main.run @@ fun env -> 214 + let net = Eio.Stdenv.net env in 197 215 let endpoint = 198 - Endpoint.create { Endpoint.default_config with port = 8080 } 216 + Endpoint.create Endpoint.default_config 199 217 |> Endpoint.plug (Plug.Session.create ~store ()) 200 218 |> Endpoint.plug (Plug.Csrf.create ()) 201 219 |> Endpoint.router routes 202 220 in 203 - Endpoint.start endpoint ~env 221 + let handler = Endpoint.to_handler endpoint in 222 + Eio.Switch.run @@ fun sw -> 223 + Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 204 224 ``` 205 225 206 226 ## Next Steps
+5 -2
docs/recipes/cors.md
··· 100 100 101 101 let () = 102 102 Eio_main.run @@ fun env -> 103 + let net = Eio.Stdenv.net env in 103 104 let cors = Plug.Cors.create 104 105 ~origins:["http://localhost:3000"] 105 106 ~methods:[`GET; `POST] ··· 108 109 () 109 110 in 110 111 let endpoint = 111 - Endpoint.create { Endpoint.default_config with port = 8080 } 112 + Endpoint.create Endpoint.default_config 112 113 |> Endpoint.plug cors 113 114 |> Endpoint.router routes 114 115 in 115 - Endpoint.start endpoint ~env 116 + let handler = Endpoint.to_handler endpoint in 117 + Eio.Switch.run @@ fun sw -> 118 + Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 116 119 ``` 117 120 118 121 Test from JavaScript:
+5 -2
docs/recipes/json-api.md
··· 163 163 164 164 let () = 165 165 Eio_main.run @@ fun env -> 166 + let net = Eio.Stdenv.net env in 166 167 let endpoint = 167 - Endpoint.create { Endpoint.default_config with port = 8080 } 168 + Endpoint.create Endpoint.default_config 168 169 |> Endpoint.plug (Plug.Cors.create ()) 169 170 |> Endpoint.router routes 170 171 in 171 - Endpoint.start endpoint ~env 172 + let handler = Endpoint.to_handler endpoint in 173 + Eio.Switch.run @@ fun sw -> 174 + Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 172 175 ``` 173 176 174 177 Test with curl:
+28 -7
docs/recipes/rate-limiting.md
··· 37 37 38 38 ## Different Limits for Different Routes 39 39 40 - Apply stricter limits to sensitive endpoints: 40 + Apply stricter limits to sensitive endpoints using per-route plugs: 41 41 42 42 ```ocaml 43 43 let login_rate_limit = ··· 49 49 ~per:60.0 (* 5 login attempts per minute *) 50 50 51 51 let routes = Router.compile [ 52 - Router.Route.post "/login" (fun params req -> 53 - Hcs.Plug.apply login_rate_limit (login_handler params) req); 52 + Router.Route.post "/login" login_handler 53 + |> Router.Route.plug login_rate_limit; 54 54 55 - Router.Route.post "/api/data" (fun params req -> 56 - Hcs.Plug.apply user_rate_limit (data_handler params) req); 55 + Router.Route.post "/api/data" data_handler 56 + |> Router.Route.plug user_rate_limit; 57 + ] 58 + ``` 59 + 60 + ## Rate Limiting with Scopes 61 + 62 + Apply rate limits to entire route groups: 63 + 64 + ```ocaml 65 + let api_rate_limit = Pipeline.create [ 66 + Plug.Rate_limit.create ~clock ~requests:100 ~per:60.0 67 + ~key:(fun _ -> Plug.Session.get "user_id" |> Option.value ~default:"anon") 68 + ] 69 + 70 + let routes = Router.compile_scopes [ 71 + Router.scope "/api" ~through:api_rate_limit [ 72 + Router.Route.get "/users" list_users; 73 + Router.Route.get "/posts" list_posts; 74 + ]; 57 75 ] 58 76 ``` 59 77 ··· 150 168 let () = 151 169 Eio_main.run @@ fun env -> 152 170 let clock = Eio.Stdenv.clock env in 171 + let net = Eio.Stdenv.net env in 153 172 let store = Plug.Session.Memory_store.create () in 154 173 let endpoint = 155 - Endpoint.create { Endpoint.default_config with port = 8080 } 174 + Endpoint.create Endpoint.default_config 156 175 |> Endpoint.plug (Plug.Session.create ~store ()) 157 176 |> Endpoint.router (routes clock) 158 177 in 159 - Endpoint.start endpoint ~env 178 + let handler = Endpoint.to_handler endpoint in 179 + Eio.Switch.run @@ fun sw -> 180 + Server.run ~sw ~net ~config:{ Server.default_config with port = 8080 } handler 160 181 ``` 161 182 162 183 ## Next Steps