A network service
std.net gives you sockets; std.http gives you a client and a server over them. This program is both: a service with three routes, started on a thread, and a client that calls each route and prints what came back. With a port on the command line it serves until you stop it.
//! A small HTTP service and the client that talks to it, in one program.
//!
//! nx run topo/code/service.nx # a self-test on a free port
//! nx run topo/code/service.nx -- 8080 # serve until killed
// NOT ON wasm32: WASI (preview 1), WebAssembly's system interface, opens no sockets.
import std.http
import std.net
import std.thread
fn health(req: *http.Request) -> http.Response {
return http.json(200, "{\"ok\":true}")
}
fn greet(req: *http.Request) -> http.Response {
var body = String.from("hello, ")
body.append(req.param("name") orelse "stranger")
return http.text(200, body)
}
fn echo(req: *http.Request) -> http.Response {
let kind = req.header("content-type") orelse "application/octet-stream"
return http.respond(200, kind, req.body)
}
fn routes() -> http.Router {
var r = http.Router.new()
r.get("/health", health)
r.get("/greet", greet)
r.post("/echo", echo)
return r
}
/// The server, on its own thread: it answers `n` requests, then stops.
struct Serving { port: u16, n: u32 }
fn serve(s: *mut Serving) {
var server = http.Server.bind("127.0.0.1", s.port) catch { return }
var r = routes()
for _ in 0..s.n { server.serve_one(&r, 5000) catch { break } }
server.close()
}
fn main() -> !void {
let args = os.args()
if args.len > 1 {
let port = try args[1].parse_int(u16)
var server = try http.Server.bind("127.0.0.1", port)
var r = routes()
println("serving on http://127.0.0.1:{}/greet?name=you", .{port})
try server.serve(&r)
return
}
// find a free port, then hand it to the server thread
var probe = try net.TcpListener.bind("127.0.0.1", 0)
let port = try probe.port()
probe.close()
var worker = thread.run(Serving, serve, Serving{ .port = port, .n = 3 })
time.sleep(200)
let base = format("http://127.0.0.1:{}", .{port})
let h = try http.get(format("{}/health", .{base})[..])
println("{} {}", .{h.status, h.body})
let g = try http.get(format("{}/greet?name=climber", .{base})[..])
println("{} {}", .{g.status, g.body})
let e = try http.post(format("{}/echo", .{base})[..], "text/plain", "ping")
println("{} {} {}", .{e.status, e.header("content-type") orelse "?", e.body})
worker.join()
}
200 {"ok":true}
200 hello, climber
200 text/plain ping
$ nx run topo/code/service.nx -- 8080
serving on http://127.0.0.1:8080/greet?name=you
Handlers and routes#
fn greet(req: *http.Request) -> http.Response {
var body = String.from("hello, ")
body.append(req.param("name") orelse "stranger")
return http.text(200, body)
}
A handler is a plain function from a request to a response. req.param reads a query parameter, req.header a header, req.body the body; http.text, http.json, http.html and http.respond(status, content_type, body) build responses, http.not_found() and http.redirect(url) the two common special ones. A Router maps a method and a path to a handler, and serve_static(dir) serves files for the paths no route claims.
Because handlers are function values, the router holds a fn(*Request) -> Response per route, and everything a handler can do is visible in its effects. A handler that allocates is normal; one that has shared_mutable is a handler that touches state other requests see, and nx effects will show it.
The server#
var server = http.Server.bind("127.0.0.1", s.port) catch { return }
var r = routes()
for _ in 0..s.n { server.serve_one(&r, 5000) catch { break } }
Server.bind listens; serve_one(&router, timeout_ms) handles one connection and returns, serve(&router) loops forever. The server handles one request at a time, which is what a tool, a local dashboard or a test needs. For the self-test it runs on a thread from chapter 16, answers three requests and stops, so the program can also be the client.
The client#
let h = try http.get(format("{}/health", .{base})[..])
println("{} {}", .{h.status, h.body})
http.get(url), http.post(url, content_type, body) and the general http.request(method, url, headers, body) speak HTTP/1.1, read bodies by Content-Length or chunked encoding, and follow redirects. Plain http:// only: TLS would need a C library, and @cImport (chapter 20) is how one is brought in.
Underneath, std.net.TcpStream.connect(host, port), send, recv, recv_all, reader() and writer() (buffered, from std.stream) are what the HTTP layer is written with, and TcpListener.bind / accept on the other side. The program uses one of these directly: binding a listener on port 0 to ask the operating system for a free port, then closing it and giving the number to the server thread.
Errors and effects#
Every call that touches the network blocks and can fail with ConnectionRefused, Timeout, NotFound (a name that does not resolve) or IoError. Timeouts are per socket, in milliseconds, and expire with error.Timeout. try and catch handle them like any other error; a service that must not go down catches at the top of its loop and keeps serving.
Next: your first GUI.
Exercises#
The compiler grades these: nx topo 17 writes them into a directory, nx topo check runs them. Or do them here: the code is editable, Run runs it with the compiler in the page (it loads on the first run and never sends your code anywhere), Check grades it as nx topo check would, and Reset puts the exercise back. Ctrl+Enter runs.
fill_greetfill in the blank
Replace ??? so /greet greets the name query parameter, or "stranger" when there is none.
import std.http
fn greet(req: *http.Request) -> http.Response {
var body = String.from("hello, ")
body.append(req.param("name") ??? "stranger")
return http.text(200, body[..])
}
fn health(req: *http.Request) -> http.Response {
_ = req
return http.text(200, "ok")
}
fn get(path: []u8, query: []u8) -> http.Request {
return http.Request{
.method = String.from("GET"), .path = String.from(path), .query = String.from(query),
.headers = List(http.Header).new(), .body = String.new(), .peer = String.from("test"),
}
}
fn main() {
var router = http.Router.new()
router.get("/greet", greet)
router.get("/health", health)
let a = get("/greet", "name=Maurice")
let b = get("/greet", "")
let c = get("/nowhere", "")
for req in [a, b, c] {
let resp = router.handle(&req)
println("{} {} {}", .{req.path, resp.status, resp.body})
}
}
The recorded output
/greet 200 hello, Maurice
/greet 200 hello, stranger
/nowhere 404 not found
Hints
req.param(name)is an optional; one keyword gives it a default (chapter 6)- a router needs no socket:
router.handle(&req)is a plain call, which is how a handler is tested
Solution
import std.http
fn greet(req: *http.Request) -> http.Response {
var body = String.from("hello, ")
body.append(req.param("name") orelse "stranger")
return http.text(200, body[..])
}
fn health(req: *http.Request) -> http.Response {
_ = req
return http.text(200, "ok")
}
fn get(path: []u8, query: []u8) -> http.Request {
return http.Request{
.method = String.from("GET"), .path = String.from(path), .query = String.from(query),
.headers = List(http.Header).new(), .body = String.new(), .peer = String.from("test"),
}
}
fn main() {
var router = http.Router.new()
router.get("/greet", greet)
router.get("/health", health)
let a = get("/greet", "name=Maurice")
let b = get("/greet", "")
let c = get("/nowhere", "")
for req in [a, b, c] {
let resp = router.handle(&req)
println("{} {} {}", .{req.path, resp.status, resp.body})
}
}
write_query_paramwrite it
Fill in param so the tests pass: the value of name in a query string like a=1&b=2,
"" for a key written without =, and null for a key that is not there.
fn param(query: []u8, name: []u8) -> ?[]u8 {
???
}
fn main() {
println("{}", .{param("q=nexium&page=2", "page") orelse "none"})
}
test "the value of a key" { expect_eq(param("q=nexium&page=2", "q") orelse "none", "nexium") }
test "a later key" { expect_eq(param("q=nexium&page=2", "page") orelse "none", "2") }
test "a key without a value" { expect_eq(param("debug&q=1", "debug") orelse "none", "") }
test "a missing key" { expect(param("q=1", "page") == null) }
test "a prefix is not the key" { expect(param("pages=3", "page") == null) }
test "an empty query" { expect(param("", "q") == null) }
The recorded output
2
Hints
query.split("&")gives the pairs;pair.find("=")is an optional index- a slice of the query is a
[]u8view, so nothing has to be copied
Solution
fn param(query: []u8, name: []u8) -> ?[]u8 {
if query.len == 0 { return null }
for pair in query.split("&") {
if let eq = pair.find("=") {
if pair[0..eq] == name { return pair[eq + 1..pair.len] }
} else if pair == name {
return pair[0..0]
}
}
return null
}
fn main() {
println("{}", .{param("q=nexium&page=2", "page") orelse "none"})
}
test "the value of a key" { expect_eq(param("q=nexium&page=2", "q") orelse "none", "nexium") }
test "a later key" { expect_eq(param("q=nexium&page=2", "page") orelse "none", "2") }
test "a key without a value" { expect_eq(param("debug&q=1", "debug") orelse "none", "") }
test "a missing key" { expect(param("q=1", "page") == null) }
test "a prefix is not the key" { expect(param("pages=3", "page") == null) }
test "an empty query" { expect(param("", "q") == null) }
Quizno peeking
nx effects: which effects doesstatuscarry?import std.http fn status(url: []u8) -> u16 { let r = http.get(url) catch { return 0 } return r.status }- (pure)
- blocks
- allocates blocks
- allocates blocks panics
Answer
allocates blocks panics. Talking to the network waits (
blocks) and builds buffers (allocates); the standard library's HTTP code can also panic, which the effect says out loud.The compiler says:
allocates: calls get at main.nx:4:13 blocks: calls get at main.nx:4:13 panics: calls get at main.nx:4:13- A handler is
fn(*Request) -> Response. How is it tested without a network?- it cannot be; a server has to run
- build a
Requestvalue and call the router'shandleon it, a plain function call - with a mock socket
Answer
build a
Requestvalue and call the router'shandleon it, a plain function call. Handlers are plain functions and the router is plain data, so a test calls them directly (the chapter's exercise does). - What does
server.serve_one(&router, 5000)do?- serves requests forever
- handles one connection, waiting at most five seconds, and returns
- serves five thousand requests
Answer
handles one connection, waiting at most five seconds, and returns.
serveloops forever;serve_onehandles one connection, which is what a self-test or a tool needs.