import http
HTTP/1.1 client and server models, JSON and HTML responses, file serving, and safe message framing.
Build bounded HTTP/HTTPS clients, JSON APIs, webpages, WebSocket/WSS services, and low-level transports with explicit errors, deadlines, and concurrency.
import httpHTTP/1.1 client and server models, JSON and HTML responses, file serving, and safe message framing.
import websocketRFC 6455 clients and server upgrades over ws:// and verified wss://.
import netTCP, UDP, typed IPv4/IPv6 addresses, DNS resolution, and millisecond timeout budgets.
import ioBounded text and binary reads, writes, append, metadata, copy, and move.
Operational model: networking calls are synchronous and bounded. Compose them with spawn for concurrency; the HTTP layer never creates hidden handler threads.
HTTP, WebSocket, and explicit file result values own their returned string fields. Release every result after its last read, on success and error paths. Resource handles have a separate lifetime: close or transfer the server, connection, client, or socket first, then release the result model.
HttpRequest.release, HttpResponse.release, and the matching HTTP server, connection, HTTPS, pending-connection, and client result release callsWebSocketResult.release and WebSocketMessage.releaseFileReadResult.release, FileWriteResult.release, and FileInfo.releasevalue.release() for standalone owned strings such as a dynamic Http.header resultval response = Http.get(url, 5000)
if response.error == 0 { println(response.body) }
HttpResponse.release(response)Consuming operation: Iron strings and models are value types. Never use a released value or a by-value alias afterwards. Response constructors clone caller inputs, so releasing a response does not invalidate the original body or headers. C callers use iron_string_release and the public Iron_*_release helpers.
Http.get returns an HttpResponse value. A zero error means transport and HTTP framing succeeded; application status remains in status.
import http
func main() {
val response = Http.get(
"http://127.0.0.1:8080/api/status",
5000,
)
if response.error != 0 {
println("request failed: {response.error} {response.error_message}")
}
if response.error == 0 {
println("HTTP {response.status} {response.reason}")
println(response.body)
}
HttpResponse.release(response)
}iron build status_client.iron -o status-client
./status-clientresponse.error != 0 means URL parsing, TCP, timeout, limit, or HTTP framing failed.response.status is meaningful when error == 0. A 404 or 500 is a valid HTTP response, not a transport error.1000..1099, TLS 3000..3099, HTTP 5000..5099, WebSocket 6000..6099, and file I/O 8000..8099.Iron uses braces for string interpolation, so literal JSON braces must be escaped.
val response = Http.post_json(
"http://127.0.0.1:8080/api/items",
"\{\"name\":\"anvil\"\}",
5000,
)
-- use response, then consume its owned strings
HttpResponse.release(response)val response = Http.request(
"PUT",
"http://127.0.0.1:8080/api/items/42",
"Authorization: Bearer dev-token\r\nContent-Type: application/json",
"\{\"name\":\"hammer\"\}",
1048576, -- maximum decoded response body
5000, -- complete request budget
)
HttpResponse.release(response)The client owns Host, Connection, Content-Length, and Transfer-Encoding. Supplying any of them in the custom block is rejected to prevent ambiguous framing.
The regular client calls accept https://. Certificate-chain and hostname/IP verification are on by default, system roots are loaded automatically, DNS names receive SNI, and TLS 1.2 is the minimum.
Secure backend: source builds enable TLS when CMake finds the OpenSSL development headers and libraries. Linux and macOS CI require verified TLS and test HTTPS compilation with a cleanly installed compiler, including Homebrew's keg-only OpenSSL. Without a development installation, HTTP/WS and plain sockets still work; HTTPS/WSS return typed error 3000 instead of falling back to plaintext.
val public_api = Http.get(
"https://example.com/api/status", 5000,
)
val private_ca = Http.get_with_ca(
"https://localhost:8443/", "cert.pem", 5000,
)
val created = Http.request_with_ca(
"POST", "https://localhost:8443/api/items",
"Content-Type: application/json", "\{\"name\":\"anvil\"\}",
1048576, "cert.pem", 5000,
)
HttpResponse.release(public_api)
HttpResponse.release(private_ca)
HttpResponse.release(created)Http.get_insecure and Http.request_insecure are explicitly unsafe development escape hatches. Never use them with production credentials.
val listening = Http.listen_tls(
"127.0.0.1", 8443, "cert.pem", "key.pem",
)
val pending = HttpsServer.accept_tcp(listening.server, 60000)
if pending.error == 0 {
val connection = pending.connection
HttpsPendingConnectionResult.release(pending)
spawn("tls-client") {
val secure = HttpsPendingConnection.handshake(connection, 5000)
if secure.error != 0 {
val error = secure.error
HttpsConnectionResult.release(secure)
return error
}
val request = HttpsConnection.read_request(
secure.connection, 16384, 1048576, 5000,
)
val error = request.error
HttpRequest.release(request)
HttpsConnection.close(secure.connection)
HttpsConnectionResult.release(secure)
return error
}
} else {
HttpsPendingConnectionResult.release(pending)
}
HttpsServer.close(listening.server)
HttpsServerResult.release(listening)accept_tcp is bounded only by the listener deadline. Each spawned handler gets a separate TLS handshake deadline, so a raw client that sends no ClientHello cannot block later clients. Handshake consumes the pending connection on either outcome; call HttpsPendingConnection.close when abandoning it. The one-step HttpsServer.accept remains available for controlled servers.
Certificate files: the certificate PEM may contain the leaf followed by intermediates. The private key must match. See the compiler-tested HTTPS server and private-CA REST client examples.
Certificates are loaded when listen_tls creates its server context. Rotate them by starting a replacement context or restarting with the new PEM files, then drain the old listener. For graceful shutdown, stop accepting, close the server, await bound handler tasks, and close their remaining connections. Keep task handles when draining matters; intentionally detached handlers cannot be awaited.
One-shot calls remain connection-closing conveniences. For chatty traffic, HttpClient explicitly owns a same-origin bounded pool, including the HTTPS verification configuration.
val opened = HttpClient.open(
"https://api.example.com", "", false, 4, 30000,
)
val first = HttpClient.request(
opened.client, "GET", "/api/status", "", "", 1048576, 5000,
)
val second = HttpClient.request(
opened.client, "GET", "/api/items", "", "", 1048576, 5000,
)
HttpClient.close(opened.client)
HttpResponse.release(first)
HttpResponse.release(second)
HttpClientResult.release(opened)The origin locks scheme, host, port, trust roots, and verification mode. It accepts only an authority with an optional trailing slash; paths, queries, fragments, and TLS-only options on plain HTTP are rejected rather than silently ignored. The connection limit bounds concurrent sockets; the idle timeout retires old entries. A stale socket is replayed once only for bodyless GET or HEAD—potentially non-idempotent requests are never retried automatically.
Server bounds: loop over read_request with an idle deadline and maximum request count, then pass request.keep_alive to send_response_keep_alive. Responses always carry Content-Length. HTTP pipelining is intentionally unsupported; graceful shutdown closes the listener first and then drains bounded handlers.
This complete server exposes a webpage, a JSON status endpoint, and a JSON POST endpoint. Each accepted connection is handled by an intentional detached spawn statement.
import http
func handle(connection: HttpConnection) -> Int {
val request = HttpConnection.read_request(
connection, 65536, 1048576, 5000,
)
if request.error != 0 {
val bad = Http.text_response(400, "bad request")
val sent = HttpConnection.send_response(connection, bad, 5000)
HttpResponse.release(bad)
HttpRequest.release(request)
HttpConnection.close(connection)
return sent
}
if request.method == "GET" and request.path == "/" {
val page = Http.html_response(200, "<h1>Hello from Iron</h1>")
val sent = HttpConnection.send_response(connection, page, 5000)
HttpResponse.release(page)
HttpRequest.release(request)
HttpConnection.close(connection)
return sent
}
if request.method == "GET" and request.path == "/api/status" {
val response = Http.json_response(200, "\{\"status\":\"ok\"\}")
val sent = HttpConnection.send_response(connection, response, 5000)
HttpResponse.release(response)
HttpRequest.release(request)
HttpConnection.close(connection)
return sent
}
if request.method == "POST" and request.path == "/api/items" {
val response = Http.json_response(201, "\{\"created\":true\}")
val sent = HttpConnection.send_response(connection, response, 5000)
HttpResponse.release(response)
HttpRequest.release(request)
HttpConnection.close(connection)
return sent
}
val missing = Http.json_response(404, "\{\"error\":\"not found\"\}")
val sent = HttpConnection.send_response(connection, missing, 5000)
HttpResponse.release(missing)
HttpRequest.release(request)
HttpConnection.close(connection)
return sent
}
func main() {
val listening = Http.listen("127.0.0.1", 8080)
if listening.error != 0 {
println("listen failed: {listening.error_message}")
HttpServerResult.release(listening)
return
}
val server = listening.server
HttpServerResult.release(listening)
while true {
val accepted = HttpServer.accept(server, 60000)
if accepted.error == 0 {
val connection = accepted.connection
HttpConnectionResult.release(accepted)
spawn("http-request") {
return handle(connection)
}
} else {
HttpConnectionResult.release(accepted)
}
}
}The repository contains the maintained, compiler-tested version at examples/networking/rest_server.iron.
| Request field | Meaning |
|---|---|
method | Validated HTTP method token such as GET or POST. |
target | Original path and query target. |
path | Path without the query string. |
query | Raw query text without the leading question mark. |
headers | Validated CRLF-separated header lines. Use Http.header for case-insensitive lookup. |
body | Decoded fixed-length or chunked request body, bounded by max_body_bytes. |
val page = Http.html_response(
200,
"<!doctype html><h1>Iron is online</h1>",
)
-- send page, then release it
HttpResponse.release(page)val asset = Http.file_response(
200,
"public/index.html",
"text/html; charset=utf-8",
1048576,
)
-- send asset, then release it
HttpResponse.release(asset)file_response reads at most the supplied limit. Choose the content type explicitly; Iron does not guess it from the filename.
Iron strings preserve embedded zero and arbitrary byte values. The byte APIs therefore carry exact binary payloads without a second buffer type.
import io
val written = IO.write_bytes("asset.bin", "Iron\0binary")
val loaded = IO.read_bytes("asset.bin", 1048576)
if loaded.error == 0 {
val info = IO.file_info("asset.bin")
println("loaded {info.size} bytes")
FileInfo.release(info)
}
val appended = IO.append_bytes("asset.bin", "\0suffix")
val copied = IO.copy_file("asset.bin", "asset-copy.bin", false)
val moved = IO.move_file("asset-copy.bin", "archive.bin", false)
FileWriteResult.release(written)
FileReadResult.release(loaded)
FileWriteResult.release(appended)
FileWriteResult.release(copied)
FileWriteResult.release(moved)read_text and read_bytes reject an oversized file before allocating. Write and append operations report committed bytes and expose open, write, flush, and close failures. copy_file and move_file require an explicit overwrite choice. With overwrite=false, move uses an atomic no-replace commit: a concurrently created destination wins intact and the source remains. Unsupported filesystems return an error instead of using a racy check-then-rename.
import websocket supports RFC 6455 clients and HTTP/HTTPS server upgrades. WSS uses the same verified TLS policy as HTTPS.
val connected = WebSocket.connect_with_protocols(
"wss://events.example.com/graphql",
"Authorization: Bearer token",
["graphql-transport-ws", "graphql-ws"],
1048576, 5000,
)
-- use/close connected.socket, then consume the result strings
WebSocketResult.release(connected)Requested protocols preserve preference order. A server uses upgrade_websocket_protocol to select zero or one exact offered token; the negotiated value is returned as connected.protocol. Unsolicited or multiple selections fail the handshake. Raw Sec-WebSocket-* overrides remain forbidden, and extensions are never negotiated until Iron implements their frame semantics.
import websocket
val connected = WebSocket.connect(
"wss://events.example.com/v1",
"Authorization: Bearer token",
1048576, 5000,
)
if connected.error == 0 {
val sent = WebSocket.send_text(connected.socket, "subscribe", 5000)
val message = WebSocket.receive(connected.socket, 30000)
val closed = WebSocket.close(connected.socket, 1000, "done", 5000)
WebSocketMessage.release(message)
}
WebSocketResult.release(connected)Message kinds are 1 text, 2 binary, 8 close, 9 ping, and 10 pong. Ping is answered automatically. Fragmented messages are reassembled under the connection limit; text and close reasons are UTF-8 validated.
val request = HttpConnection.read_request(connection, 16384, 1024, 5000)
val upgraded = HttpConnection.upgrade_websocket(
connection, request, 1048576, 5000,
)
HttpRequest.release(request)
-- use/close upgraded.socket, then release the model
WebSocketResult.release(upgraded)Ownership: a successful upgrade transfers the HTTP connection to the WebSocket. Use one receiving task and any number of sending tasks; frame writes are serialized. Do not race close or abort with another operation.
A standalone spawn statement is intentionally detached and uses the selected runtime pool. Bind it when the parent must await completion or collect a result.
spawn("connection") {
return handle(accepted.connection)
}val first = spawn("client-1") { return Http.get(url1, 5000) }
val second = spawn("client-2") { return Http.get(url2, 5000) }
val a = await first
val b = await second
HttpResponse.release(a)
HttpResponse.release(b)Host.Transfer-Encoding plus Content-Length are rejected.TcpSocket.read returns up to the caller-selected limit as an immutable, binary-safe String. Embedded zero bytes are preserved.
import net
val (socket, dial_error) = Net.tcp_dial("127.0.0.1", 9000, 2000)
if dial_error.code != 0 { return }
val (written, write_error) = TcpSocket.write(socket, "ping", 1000)
val (payload, read_error) = TcpSocket.read(socket, 65536, 1000)
if read_error.code == 0 {
println(payload)
}
TcpSocket.close(socket)TCP is a byte stream: one write does not guarantee one read. Loop until your protocol delimiter or expected length is complete, and treat an empty successful read as EOF.
Bind datagram sockets, send to typed IPv4 or IPv6 addresses, and receive a bounded UdpPacket containing the binary-safe payload plus the sender's numeric address and port.
import net
val (receiver, bind_error) = Net.udp_bind("127.0.0.1", 5353)
val (sender, sender_error) = Net.udp_bind("127.0.0.1", 0)
val (target, parse_error) = IPv4Addr.parse("127.0.0.1")
val (sent, send_error) = Net.udp_sendto_v4(sender, "ping", target, 5353, 1000)
val packet = UdpSocket.recvfrom(receiver, 65536, 1000)
if packet.error.code == 0 {
println("{packet.address}:{packet.port} {packet.data}")
}
UdpSocket.close(sender)
UdpSocket.close(receiver)Datagram boundaries are preserved: one successful receive consumes one packet. When a packet is larger than max_bytes, truncated is 1, data contains the captured prefix, and error.code is 1018. A zero-length packet is still successful; a timeout uses code 1004.
val (addresses, error) = Net.lookup_host("example.com", 2000)
if error.code != 0 {
println("DNS failed: {error.code}")
return
}
for address in addresses {
match address {
Address.V4(v4) -> println(IPv4Addr.format(v4))
Address.V6(v6) -> println(IPv6Addr.format(v6))
}
}| API | Use |
|---|---|
Http.get(url, timeout) | Bounded GET convenience call. |
Http.post_json(url, body, timeout) | POST with JSON content type. |
Http.request(...) | Custom method, headers, body, body limit, and timeout. |
Http.request_with_ca(...) | Custom verified HTTPS request with a private trust root. |
Http.request_insecure(...) | Explicitly unsafe custom request for local development only. |
Http.listen(host, port) | Create a TCP-backed HTTP server. |
Http.listen_tls(...) | Create a TLS-backed HTTPS server from a PEM chain and key. |
Http.get_with_ca(...) | Verified HTTPS GET with a private/test trust root. |
HttpServer.accept(server, timeout) | Accept one connection. |
HttpConnection.read_request(...) | Parse one bounded HTTP/1.1 request. |
HttpConnection.send_response(...) | Send a complete response with owned framing. |
Http.json_response | JSON response constructor. |
Http.html_response | UTF-8 HTML response constructor. |
Http.file_response | Bounded file-backed response. |
Http.header(headers, name) | Case-insensitive header lookup. |
WebSocket.connect(...) | Open a ws:// or verified wss:// client. |
upgrade_websocket(...) | Validate an HTTP upgrade and transfer connection ownership. |
WebSocket.send_text/send_bytes | Send a serialized text or binary frame. |
WebSocket.receive | Receive a bounded message or control frame. |
IO.read_bytes/write_bytes | Bounded, exact binary file I/O. |
IO.file_info/copy_file/move_file | Metadata and explicit-overwrite file operations. |
accept_tcp, then run each bounded TLS handshake in its handler task.● Verified remotely TCP, UDP, IP, DNS, HTTP/HTTPS clients and servers, REST JSON, webpage serving, WebSocket/WSS (including one secure reader with 24 secure writers), exact binary files, and concurrent spawn/await composition pass the Linux x86_64 Debug and Release suites.
Platform note: verified TLS and installed-compiler HTTPS builds run in Linux and macOS CI. The reproducible silvaserver.local validation image includes the matching Clang 14 ASan/UBSan/TSan runtimes and OpenSSL development files; its focused networking, concurrency, and file battery passes 8/8 under ASan/UBSan with LeakSanitizer enabled and 8/8 under TSan. CMake fails incomplete sanitizer toolchains during configuration. Native Windows is not yet a supported Iron compiler target; use WSL there. Without OpenSSL development headers, plain networking remains available and secure calls return a typed TLS-unavailable error.
See the maintained validation matrix and networking examples for source-level evidence.