2026-01-26 05:12:14 +01:00
{# retoor < retoor @ molodetz . nl > #}
{% extends 'page.html' %}
2026-01-25 02:47:47 +01:00
2026-01-26 05:12:14 +01:00
{% set page_title = "web" %}
{% set breadcrumb = [{"url": "api/index.html", "title": "API Reference"}, {"title": "web"}] %}
{% set prev_page = {"url": "api/wdantic.html", "title": "wdantic"} %}
{% set next_page = {"url": "api/websocket.html", "title": "websocket"} %}
{% block article %}
2026-01-25 02:47:47 +01:00
< h1 > web</ h1 >
< p > The < code > web</ code > module provides a web framework for building HTTP servers and a client for making HTTP requests. It includes routing, middleware, sessions, static file serving, and class-based views.</ p >
2026-01-25 19:02:02 +01:00
< pre >< code > import "web" for Application, Router, Request, Response, View, Session, Client, WebSocketResponse</ code ></ pre >
2026-01-25 02:47:47 +01:00
< h2 > Application Class</ h2 >
< div class = "class-header" >
< h3 > Application</ h3 >
< p > HTTP server with routing and middleware</ p >
</ div >
< h3 > Constructor</ h3 >
< div class = "method-signature" >
< span class = "method-name" > Application.new</ span > () → < span class = "type" > Application</ span >
</ div >
< p > Creates a new web application.</ p >
< h3 > Properties</ h3 >
< div class = "method-signature" >
< span class = "method-name" > router</ span > → < span class = "type" > Router</ span >
</ div >
< p > Access to the underlying router.</ p >
< div class = "method-signature" >
< span class = "method-name" > sessionStore</ span > → < span class = "type" > SessionStore</ span >
</ div >
< p > Access to the session storage.</ p >
< h3 > Routing Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > get</ span > (< span class = "param" > path</ span > , < span class = "param" > handler</ span > ) → < span class = "type" > Application</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > post</ span > (< span class = "param" > path</ span > , < span class = "param" > handler</ span > ) → < span class = "type" > Application</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > put</ span > (< span class = "param" > path</ span > , < span class = "param" > handler</ span > ) → < span class = "type" > Application</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > delete</ span > (< span class = "param" > path</ span > , < span class = "param" > handler</ span > ) → < span class = "type" > Application</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > patch</ span > (< span class = "param" > path</ span > , < span class = "param" > handler</ span > ) → < span class = "type" > Application</ span >
</ div >
< p > Register route handlers. Handler receives < code > Request</ code > and returns < code > Response</ code > .</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > path</ span > < span class = "param-type" > (String)</ span > - URL path with optional parameters (< code > :param</ code > ) or wildcards (< code > *</ code > )</ li >
< li >< span class = "param-name" > handler</ span > < span class = "param-type" > (Fn)</ span > - Function receiving Request, returning Response</ li >
</ ul >
< div class = "method-signature" >
< span class = "method-name" > addView</ span > (< span class = "param" > path</ span > , < span class = "param" > viewClass</ span > ) → < span class = "type" > Application</ span >
</ div >
< p > Registers a class-based view for all HTTP methods.</ p >
< div class = "method-signature" >
< span class = "method-name" > static_</ span > (< span class = "param" > prefix</ span > , < span class = "param" > directory</ span > ) → < span class = "type" > Application</ span >
</ div >
< p > Serves static files from a directory.</ p >
< pre >< code > app.static_("/assets", "./public")</ code ></ pre >
2026-01-25 19:02:02 +01:00
< div class = "method-signature" >
< span class = "method-name" > websocket</ span > (< span class = "param" > path</ span > , < span class = "param" > handler</ span > ) → < span class = "type" > Application</ span >
</ div >
< p > Registers a WebSocket handler for the given path. Handler receives Request and should use WebSocketResponse to handle the connection.</ p >
< pre >< code > app.websocket("/ws", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isText) ws.send("Echo: " + msg.text)
}
return ws
})</ code ></ pre >
2026-01-25 02:47:47 +01:00
< div class = "method-signature" >
< span class = "method-name" > use</ span > (< span class = "param" > middleware</ span > ) → < span class = "type" > Application</ span >
</ div >
< p > Adds middleware function. Middleware receives Request, returns Response or null to continue.</ p >
< div class = "method-signature" >
< span class = "method-name" > run</ span > (< span class = "param" > host</ span > , < span class = "param" > port</ span > )
</ div >
< p > Starts the server (blocking).</ p >
< h2 > Request Class</ h2 >
< div class = "class-header" >
< h3 > Request</ h3 >
< p > Incoming HTTP request</ p >
</ div >
< h3 > Properties</ h3 >
< table >
< tr >
< th > Property</ th >
< th > Type</ th >
< th > Description</ th >
</ tr >
< tr >
< td >< code > method</ code ></ td >
< td > String</ td >
< td > HTTP method (GET, POST, etc.)</ td >
</ tr >
< tr >
< td >< code > path</ code ></ td >
< td > String</ td >
< td > Request path without query string</ td >
</ tr >
< tr >
< td >< code > query</ code ></ td >
< td > Map</ td >
< td > Parsed query parameters</ td >
</ tr >
< tr >
< td >< code > headers</ code ></ td >
< td > Map</ td >
< td > Request headers</ td >
</ tr >
< tr >
< td >< code > body</ code ></ td >
< td > String</ td >
< td > Raw request body</ td >
</ tr >
< tr >
< td >< code > params</ code ></ td >
< td > Map</ td >
< td > Route parameters from URL</ td >
</ tr >
< tr >
< td >< code > cookies</ code ></ td >
< td > Map</ td >
< td > Parsed cookies</ td >
</ tr >
< tr >
< td >< code > session</ code ></ td >
< td > Session</ td >
< td > Session data</ td >
</ tr >
</ table >
< h3 > Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > header</ span > (< span class = "param" > name</ span > ) → < span class = "type" > String|null</ span >
</ div >
< p > Gets header value (case-insensitive).</ p >
< div class = "method-signature" >
< span class = "method-name" > json</ span > → < span class = "type" > Map|List</ span >
</ div >
< p > Parses body as JSON (cached).</ p >
< div class = "method-signature" >
< span class = "method-name" > form</ span > → < span class = "type" > Map</ span >
</ div >
< p > Parses body as form data (cached).</ p >
< h2 > Response Class</ h2 >
< div class = "class-header" >
< h3 > Response</ h3 >
< p > HTTP response builder</ p >
</ div >
< h3 > Static Factory Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > Response.text</ span > (< span class = "param" > content</ span > ) → < span class = "type" > Response</ span >
</ div >
< p > Creates plain text response.</ p >
< div class = "method-signature" >
< span class = "method-name" > Response.html</ span > (< span class = "param" > content</ span > ) → < span class = "type" > Response</ span >
</ div >
< p > Creates HTML response.</ p >
< div class = "method-signature" >
< span class = "method-name" > Response.json</ span > (< span class = "param" > data</ span > ) → < span class = "type" > Response</ span >
</ div >
< p > Creates JSON response (automatically stringifies).</ p >
< div class = "method-signature" >
< span class = "method-name" > Response.redirect</ span > (< span class = "param" > url</ span > ) → < span class = "type" > Response</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > Response.redirect</ span > (< span class = "param" > url</ span > , < span class = "param" > status</ span > ) → < span class = "type" > Response</ span >
</ div >
< p > Creates redirect response (default 302).</ p >
< div class = "method-signature" >
< span class = "method-name" > Response.file</ span > (< span class = "param" > path</ span > ) → < span class = "type" > Response</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > Response.file</ span > (< span class = "param" > path</ span > , < span class = "param" > contentType</ span > ) → < span class = "type" > Response</ span >
</ div >
< p > Serves a file. Auto-detects content type if not specified.</ p >
< h3 > Instance Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > header</ span > (< span class = "param" > name</ span > , < span class = "param" > value</ span > ) → < span class = "type" > Response</ span >
</ div >
< p > Sets a response header.</ p >
< div class = "method-signature" >
< span class = "method-name" > cookie</ span > (< span class = "param" > name</ span > , < span class = "param" > value</ span > ) → < span class = "type" > Response</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > cookie</ span > (< span class = "param" > name</ span > , < span class = "param" > value</ span > , < span class = "param" > options</ span > ) → < span class = "type" > Response</ span >
</ div >
< p > Sets a cookie. Options: < code > path</ code > , < code > maxAge</ code > , < code > httpOnly</ code > , < code > secure</ code > .</ p >
< h3 > Properties</ h3 >
< div class = "method-signature" >
< span class = "method-name" > status</ span > → < span class = "type" > Num</ span >
</ div >
< p > Gets or sets HTTP status code.</ p >
< div class = "method-signature" >
< span class = "method-name" > body</ span > → < span class = "type" > String</ span >
</ div >
< p > Gets or sets response body.</ p >
< h2 > View Class</ h2 >
< div class = "class-header" >
< h3 > View</ h3 >
< p > Base class for class-based views</ p >
</ div >
< p > Subclass and override methods for each HTTP method:</ p >
< pre >< code > class UserView is View {
get(request) {
return Response.json({"users": []})
}
post(request) {
var data = request.json
return Response.json({"created": data})
}
2026-01-25 19:02:02 +01:00
websocket(request) {
var ws = WebSocketResponse.new()
ws.prepare(request)
ws.iterate(Fn.new { |msg|
if (msg.isText) ws.send("Echo: " + msg.text)
})
return ws
}
2026-01-25 02:47:47 +01:00
}</ code ></ pre >
2026-01-25 19:02:02 +01:00
< h2 > WebSocketResponse Class</ h2 >
< div class = "class-header" >
< h3 > WebSocketResponse</ h3 >
< p > Server-side WebSocket connection handler (aiohttp-style API)</ p >
</ div >
< p > The < code > WebSocketResponse</ code > class provides a server-side WebSocket handler following the aiohttp-style API pattern. It handles the WebSocket handshake, frame encoding/decoding, and provides convenient methods for sending and receiving messages.</ p >
< h3 > Constructor</ h3 >
< div class = "method-signature" >
< span class = "method-name" > WebSocketResponse.new</ span > () → < span class = "type" > WebSocketResponse</ span >
</ div >
< p > Creates a new WebSocket response handler. Call < code > prepare(request)</ code > to complete the handshake before sending or receiving messages.</ p >
< h3 > Properties</ h3 >
< table >
< tr >
< th > Property</ th >
< th > Type</ th >
< th > Description</ th >
</ tr >
< tr >
< td >< code > isPrepared</ code ></ td >
< td > Bool</ td >
< td > True if the WebSocket handshake has been performed</ td >
</ tr >
< tr >
< td >< code > isOpen</ code ></ td >
< td > Bool</ td >
< td > True if the connection is open and ready for messages</ td >
</ tr >
</ table >
< h3 > Connection Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > prepare</ span > (< span class = "param" > request</ span > ) → < span class = "type" > WebSocketResponse</ span >
</ div >
< p > Performs the WebSocket handshake using the request's socket. Must be called before sending or receiving messages. Returns < code > this</ code > for method chaining.</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > request</ span > < span class = "param-type" > (Request)</ span > - The HTTP request containing the WebSocket upgrade headers</ li >
</ ul >
< pre >< code > var ws = WebSocketResponse.new()
ws.prepare(request)</ code ></ pre >
< h3 > Sending Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > send</ span > (< span class = "param" > text</ span > )
</ div >
< div class = "method-signature" >
< span class = "method-name" > sendText</ span > (< span class = "param" > text</ span > )
</ div >
< p > Sends a text message to the client. Both methods are equivalent.</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > text</ span > < span class = "param-type" > (String)</ span > - Text message to send</ li >
</ ul >
< pre >< code > ws.send("Hello, client!")
ws.sendText("Another message")</ code ></ pre >
< div class = "method-signature" >
< span class = "method-name" > sendJson</ span > (< span class = "param" > data</ span > )
</ div >
< p > Serializes the data to JSON and sends it as a text message. Convenience method for JSON-based protocols.</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > data</ span > < span class = "param-type" > (Map|List)</ span > - Data to serialize and send</ li >
</ ul >
< pre >< code > ws.sendJson({"type": "update", "value": 42})
ws.sendJson(["item1", "item2", "item3"])</ code ></ pre >
< div class = "method-signature" >
< span class = "method-name" > sendBinary</ span > (< span class = "param" > bytes</ span > )
</ div >
< p > Sends a binary message to the client.</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > bytes</ span > < span class = "param-type" > (List)</ span > - List of bytes to send</ li >
</ ul >
< pre >< code > ws.sendBinary([0x01, 0x02, 0x03, 0x04])
var imageData = File.readBytes("image.png")
ws.sendBinary(imageData)</ code ></ pre >
< h3 > Receiving Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > receive</ span > () → < span class = "type" > WebSocketMessage|null</ span >
</ div >
< p > Receives the next message from the client. Blocks until a message arrives. Returns a < code > WebSocketMessage</ code > object, or < code > null</ code > if the connection closed unexpectedly. Ping frames are automatically answered with pong.</ p >
< pre >< code > var msg = ws.receive()
if (msg != null && msg.isText) {
System.print("Got: %(msg.text)")
}</ code ></ pre >
< div class = "method-signature" >
< span class = "method-name" > receiveJson</ span > () → < span class = "type" > Map|List|WebSocketMessage|null</ span >
</ div >
< p > Receives a text message and parses it as JSON. Returns the parsed JSON data, the close frame if the connection was closed, or < code > null</ code > if the connection closed unexpectedly. Aborts if a binary frame is received.</ p >
< pre >< code > var data = ws.receiveJson()
if (data is Map && data.containsKey("type")) {
if (data["type"] == "message") {
System.print("Message: %(data["text"])")
}
}</ code ></ pre >
< div class = "method-signature" >
< span class = "method-name" > receiveText</ span > () → < span class = "type" > String|null</ span >
</ div >
< p > Receives a message and returns only the text content. Returns < code > null</ code > if the message is not a text frame, if the connection was closed, or if the connection closed unexpectedly.</ p >
< pre >< code > var text = ws.receiveText()
if (text != null) {
System.print("Received: %(text)")
}</ code ></ pre >
< div class = "method-signature" >
< span class = "method-name" > receiveBinary</ span > () → < span class = "type" > List|null</ span >
</ div >
< p > Receives a message and returns only the binary content. Returns < code > null</ code > if the message is not a binary frame, if the connection was closed, or if the connection closed unexpectedly.</ p >
< pre >< code > var bytes = ws.receiveBinary()
if (bytes != null) {
System.print("Received %(bytes.count) bytes")
}</ code ></ pre >
< div class = "method-signature" >
< span class = "method-name" > iterate</ span > (< span class = "param" > callback</ span > )
</ div >
< p > Helper method that loops while the connection is open, calling the callback for each received message (excluding close frames). Simplifies the common receive loop pattern.</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > callback</ span > < span class = "param-type" > (Fn)</ span > - Function to call with each WebSocketMessage</ li >
</ ul >
< pre >< code > ws.iterate(Fn.new { |msg|
if (msg.isText) {
ws.send("Echo: " + msg.text)
}
})</ code ></ pre >
< h3 > Control Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > ping</ span > ()
</ div >
< div class = "method-signature" >
< span class = "method-name" > ping</ span > (< span class = "param" > data</ span > )
</ div >
< p > Sends a ping frame to check if the client is still connected. Payload must be 125 bytes or less.</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > data</ span > < span class = "param-type" > (String|List)</ span > - Optional payload data</ li >
</ ul >
< div class = "method-signature" >
< span class = "method-name" > pong</ span > (< span class = "param" > data</ span > )
</ div >
< p > Sends a pong frame. Usually automatic in response to ping, but can be sent manually.</ p >
< div class = "method-signature" >
< span class = "method-name" > close</ span > ()
</ div >
< div class = "method-signature" >
< span class = "method-name" > close</ span > (< span class = "param" > code</ span > , < span class = "param" > reason</ span > )
</ div >
< p > Closes the WebSocket connection. Default code is 1000 (normal closure).</ p >
< ul class = "param-list" >
< li >< span class = "param-name" > code</ span > < span class = "param-type" > (Num)</ span > - Close status code (1000-4999)</ li >
< li >< span class = "param-name" > reason</ span > < span class = "param-type" > (String)</ span > - Human-readable close reason</ li >
</ ul >
< pre >< code > ws.close()
ws.close(1000, "Goodbye")</ code ></ pre >
< h3 > WebSocketMessage Reference</ h3 >
< p > The < code > receive()</ code > method returns a < code > WebSocketMessage</ code > object with the following properties:</ p >
< table >
< tr >
< th > Property</ th >
< th > Type</ th >
< th > Description</ th >
</ tr >
< tr >
< td >< code > opcode</ code ></ td >
< td > Num</ td >
< td > Frame opcode (1=text, 2=binary, 8=close, 9=ping, 10=pong)</ td >
</ tr >
< tr >
< td >< code > fin</ code ></ td >
< td > Bool</ td >
< td > True if this is the final fragment</ td >
</ tr >
< tr >
< td >< code > isText</ code ></ td >
< td > Bool</ td >
< td > True if this is a text frame</ td >
</ tr >
< tr >
< td >< code > isBinary</ code ></ td >
< td > Bool</ td >
< td > True if this is a binary frame</ td >
</ tr >
< tr >
< td >< code > isClose</ code ></ td >
< td > Bool</ td >
< td > True if this is a close frame</ td >
</ tr >
< tr >
< td >< code > isPing</ code ></ td >
< td > Bool</ td >
< td > True if this is a ping frame</ td >
</ tr >
< tr >
< td >< code > isPong</ code ></ td >
< td > Bool</ td >
< td > True if this is a pong frame</ td >
</ tr >
< tr >
< td >< code > text</ code ></ td >
< td > String</ td >
< td > Payload as string (for text frames)</ td >
</ tr >
< tr >
< td >< code > bytes</ code ></ td >
< td > List</ td >
< td > Payload as byte list</ td >
</ tr >
< tr >
< td >< code > closeCode</ code ></ td >
< td > Num</ td >
< td > Close status code (for close frames)</ td >
</ tr >
< tr >
< td >< code > closeReason</ code ></ td >
< td > String</ td >
< td > Close reason text (for close frames)</ td >
</ tr >
</ table >
< h3 > Close Codes Reference</ h3 >
< table >
< tr >
< th > Code</ th >
< th > Meaning</ th >
</ tr >
< tr >
< td > 1000</ td >
< td > Normal closure</ td >
</ tr >
< tr >
< td > 1001</ td >
< td > Going away (server shutdown)</ td >
</ tr >
< tr >
< td > 1002</ td >
< td > Protocol error</ td >
</ tr >
< tr >
< td > 1003</ td >
< td > Unsupported data type</ td >
</ tr >
< tr >
< td > 1005</ td >
< td > No status received</ td >
</ tr >
< tr >
< td > 1006</ td >
< td > Abnormal closure</ td >
</ tr >
< tr >
< td > 1011</ td >
< td > Server error</ td >
</ tr >
< tr >
< td > 4000-4999</ td >
< td > Application-specific codes</ td >
</ tr >
</ table >
< div class = "admonition note" >
< div class = "admonition-title" > Note</ div >
< p > Ping frames received from clients are automatically answered with pong frames. You typically do not need to handle ping/pong manually unless implementing custom keepalive logic.</ p >
</ div >
2026-01-25 02:47:47 +01:00
< h2 > Session Class</ h2 >
< div class = "class-header" >
< h3 > Session</ h3 >
< p > Session data storage</ p >
</ div >
< h3 > Properties and Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > id</ span > → < span class = "type" > String</ span >
</ div >
< p > The session ID.</ p >
< div class = "method-signature" >
< span class = "method-name" > [key]</ span > → < span class = "type" > any</ span >
</ div >
< p > Gets session value.</ p >
< div class = "method-signature" >
< span class = "method-name" > [key]=</ span > (< span class = "param" > value</ span > )
</ div >
< p > Sets session value.</ p >
< div class = "method-signature" >
< span class = "method-name" > remove</ span > (< span class = "param" > key</ span > )
</ div >
< p > Removes a session key.</ p >
< h2 > Client Class</ h2 >
< div class = "class-header" >
< h3 > Client</ h3 >
< p > HTTP client for making requests</ p >
</ div >
< h3 > Static Methods</ h3 >
< div class = "method-signature" >
< span class = "method-name" > Client.get</ span > (< span class = "param" > url</ span > ) → < span class = "type" > Map</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > Client.get</ span > (< span class = "param" > url</ span > , < span class = "param" > options</ span > ) → < span class = "type" > Map</ span >
</ div >
< p > Makes GET request.</ p >
< div class = "method-signature" >
< span class = "method-name" > Client.post</ span > (< span class = "param" > url</ span > ) → < span class = "type" > Map</ span >
</ div >
< div class = "method-signature" >
< span class = "method-name" > Client.post</ span > (< span class = "param" > url</ span > , < span class = "param" > options</ span > ) → < span class = "type" > Map</ span >
</ div >
< p > Makes POST request.</ p >
< div class = "method-signature" >
< span class = "method-name" > Client.put</ span > (< span class = "param" > url</ span > , < span class = "param" > options</ span > ) → < span class = "type" > Map</ span >
</ div >
< p > Makes PUT request.</ p >
< div class = "method-signature" >
< span class = "method-name" > Client.delete</ span > (< span class = "param" > url</ span > , < span class = "param" > options</ span > ) → < span class = "type" > Map</ span >
</ div >
< p > Makes DELETE request.</ p >
< h3 > Client Options</ h3 >
< table >
< tr >
< th > Option</ th >
< th > Type</ th >
< th > Description</ th >
</ tr >
< tr >
< td >< code > headers</ code ></ td >
< td > Map</ td >
< td > Request headers</ td >
</ tr >
< tr >
< td >< code > body</ code ></ td >
< td > String</ td >
< td > Request body</ td >
</ tr >
< tr >
< td >< code > json</ code ></ td >
< td > Map/List</ td >
< td > JSON body (auto-stringifies and sets Content-Type)</ td >
</ tr >
</ table >
< h3 > Response Format</ h3 >
< pre >< code > {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": "{...}"
}</ code ></ pre >
< h2 > Examples</ h2 >
< h3 > Basic Server</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/", Fn.new { |req|
return Response.html("< h1> Hello World< /h1> ")
})
app.get("/api/users", Fn.new { |req|
return Response.json({"users": ["Alice", "Bob"]})
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Route Parameters</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/users/:id", Fn.new { |req|
var userId = req.params["id"]
return Response.json({"id": userId})
})
app.get("/files/*", Fn.new { |req|
return Response.text("Wildcard route")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Class-Based Views</ h3 >
< pre >< code > import "web" for Application, Response, View
class ArticleView is View {
get(request) {
return Response.json({"articles": []})
}
post(request) {
var data = request.json
return Response.json({"created": data})
}
}
var app = Application.new()
app.addView("/articles", ArticleView)
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Sessions</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/login", Fn.new { |req|
req.session["user"] = "Alice"
return Response.text("Logged in")
})
app.get("/profile", Fn.new { |req|
var user = req.session["user"]
if (user == null) {
return Response.redirect("/login")
}
return Response.text("Hello, %(user)")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Middleware</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.use(Fn.new { |req|
System.print("%(req.method) %(req.path)")
return null
})
app.use(Fn.new { |req|
if (req.path.startsWith("/admin") && !req.session["isAdmin"]) {
return Response.redirect("/login")
}
return null
})
app.get("/", Fn.new { |req|
return Response.text("Home")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > HTTP Client</ h3 >
< pre >< code > import "web" for Client
import "json" for Json
var response = Client.get("https://api.example.com/data")
System.print("Status: %(response["status"])")
System.print("Body: %(response["body"])")
var postResponse = Client.post("https://api.example.com/users", {
"json": {"name": "Alice", "email": "alice@example.com"}
})
var data = Json.parse(postResponse["body"])</ code ></ pre >
2026-01-25 19:02:02 +01:00
< h3 > Concurrent HTTP Requests</ h3 >
< p > Use the < code > async</ code > keyword to create futures and < code > await</ code > to wait for results. Each request runs in its own fiber and executes in parallel while waiting for I/O.</ p >
< pre >< code > import "web" for Client
import "scheduler" for Scheduler, Future
var urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments"
]
var futures = []
for (url in urls) {
futures.add(async { Client.get(url) })
}
var responses = []
for (future in futures) {
responses.add(await future)
}
for (i in 0...urls.count) {
System.print("%(urls[i]): %(responses[i]["status"])")
}</ code ></ pre >
< h3 > Parallel Batch Requests</ h3 >
< p > For batch operations, create all futures first, then await them. The requests execute concurrently.</ p >
< pre >< code > import "web" for Client
import "scheduler" for Scheduler, Future
import "json" for Json
var fetchUrl = async { |url| Client.get(url) }
class BatchClient {
static getAll(urls) {
var futures = []
for (url in urls) {
futures.add(async { Client.get(url) })
}
var results = []
for (f in futures) {
results.add(await f)
}
return results
}
static postAll(requests) {
var futures = []
for (req in requests) {
futures.add(async { Client.post(req["url"], req["options"]) })
}
var results = []
for (f in futures) {
results.add(await f)
}
return results
}
}
var urls = [
"https://api.example.com/endpoint1",
"https://api.example.com/endpoint2",
"https://api.example.com/endpoint3"
]
var results = BatchClient.getAll(urls)
for (result in results) {
System.print("Status: %(result["status"])")
}</ code ></ pre >
< h3 > Parameterized Async Functions</ h3 >
< p > Create reusable async functions with parameters using < code > async { |args| ... }</ code > . There are two ways to invoke these functions:</ p >
< ul >
< li >< strong >< code > await fn(args)</ code ></ strong > — Direct call, waits immediately (sequential execution)</ li >
< li >< strong >< code > fn.call(args)</ code ></ strong > — Returns a Future, starts execution without waiting (concurrent execution)</ li >
</ ul >
< pre >< code > import "web" for Client
import "scheduler" for Scheduler, Future
import "json" for Json
var fetchJson = async { |url|
var response = Client.get(url)
return Json.parse(response["body"])
}
var postJson = async { |url, data|
var response = Client.post(url, {"json": data})
return Json.parse(response["body"])
}
// DIRECT CALLING (preferred for sequential operations)
// Waits for each request to complete before continuing
var user = await fetchJson("https://api.example.com/user/1")
System.print(user["name"])
// CONCURRENT EXECUTION (use .call() to start without waiting)
// Both requests run at the same time
var f1 = fetchJson.call("https://api.example.com/posts")
var f2 = fetchJson.call("https://api.example.com/comments")
// Now wait for results
var posts = await f1
var comments = await f2</ code ></ pre >
< div class = "admonition tip" >
< div class = "admonition-title" > Direct Calling vs .call()</ div >
< p > Use < code > await fn(args)</ code > when you want sequential execution. Use < code > fn.call(args)</ code > when you want to start multiple operations concurrently, then < code > await</ code > their results later.</ p >
</ div >
< h3 > WebSocket Echo Server</ h3 >
< pre >< code > import "web" for Application, Response, WebSocketResponse
var app = Application.new()
app.get("/", Fn.new { |req|
return Response.html("< script> var ws=new WebSocket('ws://localhost:8080/ws');ws.onmessage=e=> console.log(e.data);ws.onopen=()=> ws.send('hello');< /script> ")
})
app.websocket("/ws", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isText) {
ws.send("Echo: " + msg.text)
}
}
ws.close()
return ws
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > WebSocket JSON API</ h3 >
< pre >< code > import "web" for Application, Response, WebSocketResponse
var app = Application.new()
app.websocket("/api", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
ws.sendJson({"type": "welcome", "message": "Connected to API"})
while (ws.isOpen) {
var data = ws.receiveJson()
if (data == null) break
if (data is Map && data["type"] == "ping") {
ws.sendJson({"type": "pong", "timestamp": data["timestamp"]})
} else if (data is Map && data["type"] == "echo") {
ws.sendJson({"type": "echo", "data": data["data"]})
}
}
return ws
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > WebSocket Binary Data Transfer</ h3 >
< pre >< code > import "web" for Application, Response, WebSocketResponse
import "io" for File
var app = Application.new()
app.websocket("/binary", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
ws.send("Ready to receive binary data")
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isBinary) {
System.print("Received %(msg.bytes.count) bytes")
ws.sendJson({"received": msg.bytes.count, "status": "ok"})
} else if (msg.isText) {
if (msg.text == "send-file") {
var data = File.readBytes("example.bin")
ws.sendBinary(data)
}
}
}
return ws
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > WebSocket in Class-Based View</ h3 >
< pre >< code > import "web" for Application, Response, View, WebSocketResponse
class ChatView is View {
get(request) {
return Response.html("< h1> Chat Room< /h1>< p> Connect via WebSocket< /p> ")
}
websocket(request) {
var ws = WebSocketResponse.new()
ws.prepare(request)
ws.sendJson({"type": "connected", "room": "general"})
ws.iterate(Fn.new { |msg|
if (msg.isText) {
var data = Json.parse(msg.text)
if (data["type"] == "message") {
ws.sendJson({
"type": "broadcast",
"from": data["from"],
"text": data["text"]
})
}
}
})
return ws
}
}
var app = Application.new()
app.addView("/chat", ChatView)
app.run("0.0.0.0", 8080)</ code ></ pre >
< div class = "admonition warning" >
< div class = "admonition-title" > WebSocket Error Handling</ div >
< p > Always check for < code > null</ code > or close frames when receiving messages. The connection can close at any time due to network issues or client disconnection. Wrap WebSocket handling in proper error handling to prevent server crashes.</ p >
</ div >
2026-01-25 02:47:47 +01:00
< div class = "admonition note" >
< div class = "admonition-title" > Note</ div >
< p > Sessions are stored in memory by default. Data is lost when the server restarts. For production, consider persisting session data to a database.</ p >
</ div >
2026-01-26 05:12:14 +01:00
{% endblock %}