HTTP, Networking, And WebSockets

HTTP

Import http for client calls and simple servers.

  • server: serve, listen, close, shutdown, serverAddr
  • client: get, post, postJson, request, requestWithOptions
  • streaming: stream, streamWrite, streamFlush, streamClose
  • helpers: parseJson, Headers, Cookie, response, jsonResponse
  • classes exported by import: Request, Response
import http;

let response = http.get("https://example.com");
io.println(response["status"]);

Recoverable HTTP transport and server I/O failures are surfaced as Geblang exceptions. Server bind failures, including port conflicts, are IOError values:

try {
    http.serve("127.0.0.1:8080", handler);
} catch (IOError e) {
    io.println("server could not start: " + e.message);
}

HTTP response statuses are not exceptions. A 404, 422 or 500 response is returned as a normal response value; inspect response["status"] and decide how your application should handle it.

Client configuration

http.newClient(opts) creates a configurable, reusable client. Most options shape the underlying connection pool and cookie behaviour:

Key Type Default Meaning
timeoutMs int none Per-request deadline (entire request lifecycle)
baseUrl string none Prefix for relative URLs passed to client.get(...) etc.
headers dict<string, string> / Headers empty Default headers added to every request
cookieJar CookieJar or true none Persist cookies across requests. Pass an explicit http.newCookieJar() to inspect/share, or true to attach a fresh jar
keepAlive bool true Reuse the underlying TCP/TLS connection. Set false to force a new connection per request (debugging only)
maxIdleConns int Go default Idle-connection pool size (per host and total)
proxy string none Forward all requests through this HTTP/HTTPS proxy URL
proxyFromEnv bool false Honour HTTP_PROXY/HTTPS_PROXY/NO_PROXY environment variables

Per-call options accepted by Client.request(opts) cover retries and timeouts in addition to the standard method, url, body, and headers:

Key Type Default Meaning
timeoutMs int client default Overrides the per-request timeout for this call
retries int 1 (no retry) Total attempts including the first
retryBackoffMs int 100 Base backoff before retry 2; doubled each retry, with full jitter
retryBackoffMaxMs int 5000 Upper bound on a single sleep
retryStatuses list [502, 503, 504, 429] Status codes that trigger a retry

A cookie jar persists Set-Cookie responses and replays them on subsequent requests to matching hosts. This is the right shape for any client that needs to maintain a session (login, CSRF tokens, server-stored basket, etc.).

let jar     = http.newCookieJar();
let session = http.newClient({"cookieJar": jar, "baseUrl": "https://api.example.com"});

session.post("/login", "{\"user\":\"ada\",\"password\":\"...\"}",
             {"Content-Type": "application/json"});

# the session cookie is automatically sent on subsequent requests
let me = session.get("/me");

# inspect or persist the cookies between runs
for (c in jar.cookies("https://api.example.com")) {
    io.println(c["name"] + "=" + c["value"]);
}

CookieJar.cookies(url) returns a list<dict> with name, value, domain, path, secure, and httpOnly. CookieJar.setCookies(url, list) populates the jar from a saved list (useful for reusing a session across process restarts). CookieJar.clear() empties the jar.

Proxies and connection control

Most production workloads benefit from a small idle pool and a proxy:

let client = http.newClient({
    "baseUrl":      "https://internal.example.com",
    "proxy":        "http://proxy.example.com:3128",
    "keepAlive":    true,
    "maxIdleConns": 32,
    "timeoutMs":    10000
});

Set "proxyFromEnv": true to defer to HTTP_PROXY/HTTPS_PROXY/NO_PROXY environment variables instead of hard-coding a URL.

User-Agent

Every outgoing HTTP request defaults to User-Agent: Geblang/1.0. Override by setting your own value either in the per-call headers argument or as a headers default on the client:

let c = http.newClient({"headers": {"User-Agent": "MyApp/2.0"}});

Network errors are always retried up to retries. When all attempts are exhausted, the last response (or the last network error) is returned to the caller — no exception is thrown.

let client = http.newClient({"timeoutMs": 5000});
let resp = client.request({
    "method": "GET",
    "url": "https://example.com/flaky",
    "retries": 5,
    "retryBackoffMs": 200,
    "retryStatuses": [502, 503, 504, 429, 500]
});

http.stream(handler) creates a low-level streaming response. The source-level web.sse.streaming(handler) wrapper gives the handler an sse.EventStream that can write and flush chunks while the request remains open:

import http;
import sys;
import web.sse as sse;

func events(dict<string, any> request): dict<string, any> {
    return sse.streaming(func(sse.EventStream stream): void {
        for (n in 1..3) {
            stream.write(sse.event("tick", "count " + (n as string), {}));
            stream.flush();
            sys.sleep(1000);
        }
        stream.close();
    });
}

Net

Import net for DNS, TCP, and UDP:

  • address helpers: joinHostPort, splitHostPort, lookupHost
  • TCP: listenTcp, connectTcp, accept, read, write, close, localAddr, remoteAddr, setDeadline, clearDeadline
  • UDP: listenUdp, dialUdp, readFrom, writeTo

For task-returning socket calls, import async.net.

Socket operations use the same error model as HTTP: connection failures, deadline failures, and bind conflicts throw IOError so applications can recover or report a clean message.

Static File Server

http.server is an executable source module. Run it with -m to serve the current directory for local development:

geblang -m http.server 8080

The optional port defaults to 8000. The server binds to 127.0.0.1, serves index.html for directories when present, and otherwise renders a simple directory listing.

WebSocket

Import websocket for low-level WebSocket client and server operations, or web.websocket for the higher-level OO interface.

Server: web.websocket

ws.upgrade(handler) returns a special HTTP response dict that triggers the WebSocket handshake when returned from a route handler. Inside the handler, conn is a Connection value with the full OO interface:

import web.router as router;
import web.websocket as ws;

let app = router.newRouter();

router.get(app, "/socket", func(dict<string, any> request): dict<string, any> {
    return ws.upgrade(func(ws.Connection conn): void {
        let msg = conn.readText();
        conn.sendText("echo: " + msg);
        conn.close();
    });
});

Connection methods: sendText, readText, sendJson, readJson, sendBytes, readBytes, close.

Client: web.websocket

import web.websocket as ws;

let conn = ws.connect("ws://127.0.0.1:8080/socket");
conn.sendJson({"message": "hello"});
let reply = conn.readJson();
conn.close();

ws.connectWithHeaders(url, headers) is available when custom headers are needed (e.g. for authentication tokens).

Low-level: websocket

For scripts that don't use the router, import the native websocket module directly. upgrade(handler) works the same way - it returns a response dict

  • but the handler receives a raw opaque handle rather than a Connection:
import http;
import websocket;

http.serve("127.0.0.1:8080", func(dict<string, any> request): dict<string, any> {
    return websocket.upgrade(func(any conn): void {
        websocket.sendText(conn, "hello " + websocket.readText(conn));
        websocket.close(conn);
    });
});

URL Parsing And Manipulation

Import url to parse, construct, and transform URLs:

import url;

let u = url.URL("https://example.com:8080/search?q=hello#results");
io.println(u.scheme());    # https
io.println(u.host());      # example.com
io.println(u.port());      # 8080
io.println(u.path());      # /search
io.println(u.fragment());  # results

let q = u.query();
io.println(q["q"]);        # hello

url.URL methods

Method Description
scheme() scheme string
host() host without port
port() port string (empty if default)
path() path component
query() dict<string, string> of query params
fragment() fragment after #
toString() full URL string
toDict() dict with scheme, host, port, path, query, fragment keys
withScheme(s) new URL with scheme replaced
withHost(h) new URL with host replaced
withPath(p) new URL with path replaced
withQuery(q) new URL with query replaced (dict or raw string)
withFragment(f) new URL with fragment replaced
resolve(ref) resolve a reference URL against this base
normalize() clean path and re-encode query string

All with* methods return new url.URL values rather than mutating in place.

Module-level functions

# parse returns a plain dict
let parts = url.parse("https://example.com/path?k=v");

# stringify reassembles a dict back into a URL string
let text = url.stringify({"scheme": "https", "host": "example.com", "path": "/users"});

# encode / decode query-string components
let encoded = url.encode("hello world");   # hello+world
let decoded = url.decode("hello+world");   # hello world

# joinPath appends path segments to a base URL string
let api = url.joinPath("https://api.example.com", "v1", "users");
# https://api.example.com/v1/users

Use url.URL for chaining or repeated access to URL parts. Use url.parse when you need a plain dict, for example to pass directly to JSON.