added nullable ?: break support with proper inference ;)
This commit is contained in:
1 parent
fae9965bdf
commit
79429d5f2d
5 files changed
+417
-104
No files matched your search
+160
-44
@@ -1,9 +1,8 @@
|
||||
### lyng.io.http.server — Minimal HTTP/1.1 and WebSocket server
|
||||
# `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
|
||||
@@ -15,9 +14,7 @@ It does not aim to replace a full reverse proxy. Typical deployment is behind ng
|
||||
|
||||
> **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
|
||||
## Install The Module Into A Lyng Session
|
||||
|
||||
Kotlin bootstrap example:
|
||||
|
||||
@@ -35,9 +32,7 @@ suspend fun bootstrapHttpServer() {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### RequestContext Sugar
|
||||
## RequestContext Sugar
|
||||
|
||||
Route handlers use `RequestContext` as the receiver, so inside handlers you normally write direct calls such as:
|
||||
|
||||
@@ -48,11 +43,9 @@ Route handlers use `RequestContext` as the receiver, so inside handlers you norm
|
||||
- `request.path`
|
||||
- `routeParams["id"]`
|
||||
|
||||
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request/exchange parameter through every route lambda.
|
||||
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request or exchange parameter through every route lambda.
|
||||
|
||||
---
|
||||
|
||||
#### JSON API Sugar
|
||||
## JSON API Sugar
|
||||
|
||||
For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
||||
|
||||
@@ -61,7 +54,7 @@ For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
||||
|
||||
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`.
|
||||
|
||||
**Typed JSON POST**
|
||||
### Typed JSON POST
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
@@ -85,7 +78,7 @@ server.postPath("/api/users") {
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
**JSON response with route params**
|
||||
### JSON Response With Route Params
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
@@ -103,9 +96,7 @@ server.getPath("/api/users/{id}") {
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Request and Route Data
|
||||
## Request And Route Data
|
||||
|
||||
`ServerRequest` exposes parsed HTTP request data:
|
||||
|
||||
@@ -131,15 +122,13 @@ server.listen(8080, "127.0.0.1")
|
||||
- `addHeader(...)`
|
||||
- `acceptWebSocket(...)`
|
||||
|
||||
For exact routes, `routeMatch` is `null` and `routeParams` is empty.
|
||||
For regex routes, `routeMatch` is set and `routeParams` is empty.
|
||||
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
|
||||
|
||||
#### Reusable Routers
|
||||
|
||||
`Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself.
|
||||
`Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself.
|
||||
Mount it into `HttpServer` or another `Router`.
|
||||
|
||||
```lyng
|
||||
@@ -164,11 +153,9 @@ 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
|
||||
|
||||
#### WebSocket Routes
|
||||
|
||||
You can route websocket upgrades by exact path, regex, or path template:
|
||||
You can route websocket upgrades by exact path, regex, or path template.
|
||||
|
||||
```lyng
|
||||
server.ws("/chat") { ws ->
|
||||
@@ -182,9 +169,147 @@ server.wsPath("/ws/{room}") { ws ->
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
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.
|
||||
|
||||
#### Path-Template Routes
|
||||
### 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"] ?: "<unknown>"
|
||||
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`.
|
||||
|
||||
@@ -198,7 +323,6 @@ server.getPath("/users/{userId}/posts/{postId}") {
|
||||
```
|
||||
|
||||
Template rules:
|
||||
|
||||
- template must start with `/`
|
||||
- a segment is either literal text or `{name}`
|
||||
- parameter names must be valid identifiers
|
||||
@@ -208,9 +332,7 @@ Template rules:
|
||||
- `+` stays `+`
|
||||
- malformed `%` stays literal
|
||||
|
||||
---
|
||||
|
||||
#### Regex Routes
|
||||
## Regex Routes
|
||||
|
||||
Regex routes match the whole request path, not a substring.
|
||||
|
||||
@@ -221,9 +343,7 @@ server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Basic Exact Route
|
||||
## Basic Exact Route
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
@@ -236,9 +356,7 @@ server.get("/hello") {
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Route Precedence
|
||||
## Route Precedence
|
||||
|
||||
Dispatch order is:
|
||||
|
||||
@@ -250,11 +368,9 @@ Dispatch order is:
|
||||
|
||||
This means exact routes stay fast and always win over template or regex routes for the same path.
|
||||
|
||||
---
|
||||
## API Surface
|
||||
|
||||
#### API Surface
|
||||
|
||||
`Router` route registration methods:
|
||||
### `Router` Route Registration Methods
|
||||
|
||||
- `get(path: String|Regex, handler)`
|
||||
- `getPath(pathTemplate: String, handler)`
|
||||
@@ -271,7 +387,7 @@ This means exact routes stay fast and always win over template or regex routes f
|
||||
- `fallback(handler)`
|
||||
- `mount(router)`
|
||||
|
||||
`HttpServer` route registration methods:
|
||||
### `HttpServer` Route Registration Methods
|
||||
|
||||
- `get(path: String|Regex, handler)`
|
||||
- `getPath(pathTemplate: String, handler)`
|
||||
|
||||
Reference in new issue
Block a user