# `lyng.io.http.server` - Minimal HTTP/1.1 And WebSocket Server This module provides a small server-side HTTP API for Lyng scripts. It is implemented in `lyngio` on top of the existing TCP layer and is intended for embedded tools, local services, test fixtures, and lightweight app backends. It supports: - HTTP/1.1 request parsing - keep-alive - exact-path routing - regex routing - path-template routing with named parameters - websocket upgrade and server-side sessions It does not aim to replace a full reverse proxy. Typical deployment is behind nginx, Caddy, or another frontend that handles TLS and public-facing edge concerns. > **Security note:** this module uses the same `NetAccessPolicy` capability model as raw TCP sockets. If scripts are allowed to listen on TCP, they can host an HTTP server. ## Install The Module Into A Lyng Session Kotlin bootstrap example: ```kotlin import net.sergeych.lyng.EvalSession import net.sergeych.lyng.Scope import net.sergeych.lyng.io.http.server.createHttpServerModule import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy suspend fun bootstrapHttpServer() { val session = EvalSession() val scope: Scope = session.getScope() createHttpServerModule(PermitAllNetAccessPolicy, scope) session.eval("import lyng.io.http.server") } ``` ## RequestContext Sugar Route handlers use `RequestContext` as the receiver, so inside handlers you normally write direct calls such as: - `jsonBody()` - `respondJson(...)` - `respondHtml { ... }` - `respondText(...)` - `setHeader(...)` - `request.path` - `routeParams["id"]` This keeps ordinary HTTP endpoints compact and avoids passing an explicit request or exchange parameter through every route lambda. ## HTML Response Sugar Use `respondHtml { ... }` to render an HTML document with the `lyng.io.html` DSL and send it as `text/html; charset=utf-8`. ```lyng import lyng.io.http.server import lyng.io.html val server = HttpServer() server.get("/") { respondHtml { head { title { +"Lyng status" } } body { h3 { +"Service is running" } p { +"Path: ${request.path}" } } } } server.listen(8080, "127.0.0.1") ``` Pass `code:` when the route should return a non-200 status: ```lyng server.get("/accepted") { respondHtml(code: 202) { body { h3 { +"Accepted" } } } } ``` ## JSON API Sugar For ordinary JSON APIs, `RequestContext` includes two primary helpers: - `jsonBody()` decodes the request body with typed `Json.decodeAs(...)` - `respondJson(body, status = 200)` sets JSON content type and responds with plain `toJsonString()` These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`. ### Typed JSON POST With Route Params ```lyng import lyng.io.http.server closed class CreateResultRequest(title: String, score: Int) closed class CreateResultResponse(id: String, userId: String, title: String, score: Int) val server = HttpServer() server.postPath("/api/users/{userId}/results") { val req = jsonBody() if (req.title.isBlank()) { respondJson({ error: "title must not be empty" }, 400) return } respondJson( CreateResultResponse("r-101", routeParams["userId"], req.title, req.score), 201 ) } server.listen(8080, "127.0.0.1") ``` ### JSON Response With Route Params ```lyng import lyng.io.http.server val server = HttpServer() server.getPath("/api/users/{id}") { respondJson({ id: routeParams["id"], path: request.path, ok: true }) } server.listen(8080, "127.0.0.1") ``` ## Request And Route Data `ServerRequest` exposes parsed HTTP request data: - `method: String` - `target: String` - `path: String` - `pathParts: List` - `queryString: String?` - `query: Map` - `headers: HttpHeaders` - `body: Buffer` `RequestContext` exposes routing context and response controls: - `request: ServerRequest` - `routeMatch: RegexMatch?` - `routeParams: Map` - `jsonBody()` - `respond(...)` - `respondText(...)` - `respondJson(body, status = 200)` - `respondHtml(code: 200) { ... }` - `setHeader(...)` - `addHeader(...)` - `acceptWebSocket(...)` For exact routes, `routeMatch` is `null` and `routeParams` is empty. For regex routes, `routeMatch` is set and `routeParams` is empty. For path-template routes, both `routeMatch` and `routeParams` are set. ## Reusable Routers `Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself. Mount it into `HttpServer` or another `Router`. ```lyng import lyng.io.http.server val api = Router() api.get("/health") { respondText(200, "ok") } val users = Router() users.getPath("/users/{id}") { respondJson({ id: routeParams["id"] }) } api.mount(users) val server = HttpServer() server.mount(api) server.listen(8080, "127.0.0.1") ``` Mounted routers reuse the built-in server router. They are configuration-time composition, not an extra per-request Lyng dispatch layer. ## WebSocket Routes You can route websocket upgrades by exact path, regex, or path template. ```lyng server.ws("/chat") { ws -> ws.sendText("hello") ws.close() } server.wsPath("/ws/{room}") { ws -> ws.sendText("room=" + routeParams["room"]) ws.close() } ``` A websocket handler runs only for requests that actually ask for websocket upgrade. Ordinary HTTP requests to the same path are not treated as websocket sessions. ### Choosing Between `ws(...)` And `acceptWebSocket(...)` Use `server.ws(...)` or `server.wsPath(...)` when the route is always a websocket endpoint. Use `acceptWebSocket(...)` inside a normal HTTP handler when the same route may inspect the request first and then decide whether to upgrade. ```lyng server.get("/maybe-upgrade") { if (!request.isWebSocketUpgrade()) { respondText(400, "websocket upgrade required") return } acceptWebSocket { ws -> ws.sendText("connected") ws.close() } } ``` ### Reading Incoming Messages Inside a websocket handler, call `ws.receive()` to wait for the next application message. What `receive()` returns: - `WsMessage` for the next text or binary message. - `null` after the client sends a close frame. - `null` after the socket is already closed and no more frames can arrive. What reaches Lyng code: - Text frames become `WsMessage(isText = true, text = ...)`. - Binary frames become `WsMessage(isText = false, data = ...)`. - Fragmented websocket messages are reassembled before they are returned. - Ping and pong control frames are handled internally and do not appear in Lyng. - A client close frame is answered by the server close handshake, then `receive()` returns `null`. Typical server receive loop: ```lyng import lyng.buffer server.ws("/echo") { ws -> while (true) { val msg = ws.receive() ?: break if (msg.isText) { ws.sendText("echo:" + msg.text) } else { ws.sendBytes(msg.data as Buffer) } } } ``` ### Sending Outgoing Messages Use: - `ws.sendText(text)` for text messages. - `ws.sendBytes(data)` for binary messages. Example: ```lyng import lyng.buffer server.ws("/push") { ws -> ws.sendText("ready") ws.sendBytes(Buffer(1, 2, 3)) ws.close() } ``` Send behavior: - Each call sends one websocket message. - The server API does not expose frame-by-frame streaming. - Once the session is closed, send calls fail with a websocket error. ### What Happens When The Connection Closes There are three practical cases: 1. The client closes first. The runtime replies with a close frame, releases the socket, and `receive()` returns `null`. 2. Your handler closes first with `ws.close(...)`. The runtime sends a close frame and releases the socket locally. 3. The transport disappears unexpectedly. The session is released and no more messages can be received; subsequent sends fail. What Lyng code should do: - Treat `receive() == null` as end-of-session. - Exit the handler or break the receive loop at that point. - Do not keep sending after close has been observed. The current server-side API does not expose the peer close code or close reason to Lyng. ### Closing The Connection Yourself Call `ws.close()` when you want to terminate the websocket session. ```lyng server.ws("/chat") { ws -> ws.sendText("server shutting down") ws.close(1000, "done") } ``` Close semantics: - `close()` sends a websocket close frame with the given code and reason. - Defaults are `code = 1000` and `reason = ""`. - `close()` is idempotent; calling it again after close does nothing. - After local close, the session should be treated as unusable. - After close, `isOpen()` becomes false and further sends fail. ### WebSocket Handler Pattern ```lyng import lyng.io.http.server val server = HttpServer() server.wsPath("/rooms/{room}") { ws -> val room = routeParams["room"] ?: "" ws.sendText("joined:" + room) while (true) { val msg = ws.receive() ?: break if (msg.isText) { ws.sendText(room + ":" + msg.text) } } ws.close() } server.listen(8080, "127.0.0.1") ``` ## Path-Template Routes Path templates are sugar on top of regex routes. Template parameters are exposed as decoded `routeParams`. ```lyng server.getPath("/users/{userId}/posts/{postId}") { respondText( 200, routeParams["userId"] + ":" + routeParams["postId"] ) } ``` Template rules: - template must start with `/` - a segment is either literal text or `{name}` - parameter names must be valid identifiers - parameter values match one path segment only - parameter values use path decoding rules: - valid percent-encoding is decoded - `+` stays `+` - malformed `%` stays literal ## Regex Routes Regex routes match the whole request path, not a substring. ```lyng server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) { val m = routeMatch!! respondText(200, "user=" + m[1] + ", post=" + m[2]) } ``` ## Basic Exact Route ```lyng import lyng.io.http.server val server = HttpServer() server.get("/hello") { setHeader("Content-Type", "text/plain") respondText(200, "hello") } server.listen(8080, "127.0.0.1") ``` ## Route Precedence Dispatch order is: 1. exact method route 2. exact `any` route 3. regex method route, registration order 4. regex `any` route, registration order 5. fallback This means exact routes stay fast and always win over template or regex routes for the same path. ## API Surface ### `Router` Route Registration Methods - `get(path: String|Regex, handler)` - `getPath(pathTemplate: String, handler)` - `post(path: String|Regex, handler)` - `postPath(pathTemplate: String, handler)` - `put(path: String|Regex, handler)` - `putPath(pathTemplate: String, handler)` - `delete(path: String|Regex, handler)` - `deletePath(pathTemplate: String, handler)` - `any(path: String|Regex, handler)` - `anyPath(pathTemplate: String, handler)` - `ws(path: String|Regex, handler)` - `wsPath(pathTemplate: String, handler)` - `fallback(handler)` - `mount(router)` ### `HttpServer` Route Registration Methods - `get(path: String|Regex, handler)` - `getPath(pathTemplate: String, handler)` - `post(path: String|Regex, handler)` - `postPath(pathTemplate: String, handler)` - `put(path: String|Regex, handler)` - `putPath(pathTemplate: String, handler)` - `delete(path: String|Regex, handler)` - `deletePath(pathTemplate: String, handler)` - `any(path: String|Regex, handler)` - `anyPath(pathTemplate: String, handler)` - `ws(path: String|Regex, handler)` - `wsPath(pathTemplate: String, handler)` - `fallback(handler)` - `mount(router)` - `listen(port, host = null, backlog = 128)`