7.0 KiB
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
NetAccessPolicycapability 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:
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<T>()respondJson(...)respondText(...)setHeader(...)request.pathrouteParams["id"]
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request/exchange parameter through every route lambda.
JSON API Sugar
For ordinary JSON APIs, RequestContext includes two primary helpers:
jsonBody<T>()decodes the request body with typedJson.decodeAs(...)respondJson(body, status = 200)sets JSON content type and responds with plaintoJsonString()
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical Json.encode(...).
Typed JSON POST
import lyng.io.http.server
closed class CreateUserRequest(name: String, age: Int)
closed class CreateUserResponse(id: Int, name: String, age: Int)
val server = HttpServer()
server.postPath("/api/users") {
val req = jsonBody<CreateUserRequest>()
if (req.name.isBlank()) {
respondJson({ error: "name must not be empty" }, 400)
return
}
respondJson(CreateUserResponse(101, req.name, req.age), 201)
}
server.listen(8080, "127.0.0.1")
JSON response with route params
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: Stringtarget: Stringpath: StringpathParts: List<String>queryString: String?query: Map<String, String>headers: HttpHeadersbody: Buffer
RequestContext exposes routing context and response controls:
request: ServerRequestrouteMatch: RegexMatch?routeParams: Map<String, String>jsonBody<T>()respond(...)respondText(...)respondJson(body, status = 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.
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:
server.ws("/chat") { ws ->
ws.sendText("hello")
ws.close()
}
server.wsPath("/ws/{room}") { ws ->
ws.sendText("room=" + routeParams["room"])
ws.close()
}
Path-Template Routes
Path templates are sugar on top of regex routes. Template parameters are exposed as decoded routeParams.
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.
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
val m = routeMatch!!
respondText(200, "user=" + m[1] + ", post=" + m[2])
}
Basic Exact Route
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:
- exact method route
- exact
anyroute - regex method route, registration order
- regex
anyroute, registration order - 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)