HTTP service
Test a JSON request handler without a socket, then serve it over local HTTP.
This guide builds a local HTTP service with a health route and a JSON user route. You will test its request handler without a socket and then run the same handler in a server. It assumes services and errors and concurrency.
Create guide-http/ at the root of the checkout, with this i.toml:
name = "guide_http"
[dependencies]
std = { path = "../std" }
[test]
collection = "std/test.tests" Write the handler
A handler takes an http.Request and returns an http.Response. Put this complete module in src/handler.i:
(
std/http
std/json
std/schema
std/bit_array
)
(
handle
)
User = (name = String)
user_schema = () {
schema.field(
schema.object(User(name = "")),
"name",
schema.string,
(user) { user.name },
(_, name) { User(name = name) },
)
}
reply = (status, value) {
base = http.response(http.Status(status))
text = json.print(value)
http.Response(
..base,
headers = http.Headers([("Content-Type", "application/json")]),
body = <<text:utf8>>,
)
}
invalid = () { reply(422, json.Object([("error", json.Text("invalid user"))])) }
handle = (req : http.Request) {
(req.method, req.target) |>
((http.Method("GET"), "/health")) {
reply(200, json.Object([("status", json.Text("ok"))]))
}
| ((http.Method("POST"), "/users")) {
bit_array.to_string(req.body) |>
(Ok(text)) {
schema.parse(user_schema(), text) |>
(Ok(user)) {
reply(
200,
schema.encode(user_schema(), user),
)
}
| (Error(_)) { invalid() }
}
| (Error(_)) { invalid() }
}
| (_) { reply(404, json.Object([("error", json.Text("not found"))])) }
} Put this complete test in test/handler_test.i:
(
testlib = (test) = std/test
std/http
guide_http/handler
)
test("health and missing route", () {
health = handler.handle(http.request(http.Method("GET"), "/health"))
assert(health.status == http.Status(200))
assert(http.body_text(health) == Ok("{\"status\":\"ok\"}"))
missing = handler.handle(http.request(http.Method("GET"), "/missing"))
assert(missing.status == http.Status(404))
})
test("JSON request", () {
http.Complete(req, <<>>) = http.feed_request(
<<>>,
<<
"POST /users HTTP/1.1\r\nContent-Length: 14\r\n\r\n{\"name\":\"Ada\"}":utf8,
>>,
)
response = handler.handle(req)
assert(response.status == http.Status(200))
assert(http.body_text(response) == Ok("{\"name\":\"Ada\"}"))
assert(
handler.handle(http.Request(..req, body = <<"{}":utf8>>)).status ==
http.Status(422),
)
assert(
handler.handle(http.Request(..req, body = <<"{":utf8>>)).status ==
http.Status(422),
)
assert(
handler.handle(http.Request(..req, body = <<255>>)).status ==
http.Status(422),
)
}) i test --opt 0
i test --opt 2 Each run prints:
pass guide_http/handler_test.health and missing route
pass guide_http/handler_test.JSON request
2 passed, 0 failed The first test constructs requests directly with http.request. The second feeds a complete HTTP/1.1 request to http.feed_request, which returns the parsed request and any unconsumed bytes. Here the body is 14 bytes and no bytes remain. Neither test opens a socket.
Read and return JSON
Request carries the method, target, headers, body bytes, and trailers. The handler matches the method and target together. These routes use exact target strings; /health?x=1 does not match /health. For segment patterns and query-independent path matching, the library also provides http.route.
The user route first checks UTF-8, then calls schema.parse. The schema maps a JSON object’s required name field to a User value. Invalid UTF-8, malformed JSON, and an object missing that field all produce the tested 422 response. For a valid user, schema.encode produces a json.Json value; json.print supplies the bytes returned to the client. Responses set Content-Type: application/json, including errors.
These operations are ordinary library calls. The handler needs no network service. Keeping it in its own module makes its inputs and outputs available to tests and to the server unchanged.
Start the service
Keep src/handler.i exactly as above. Add this complete src/main.i to the same project:
(
i/net
i/task
i/stdio
std/http
guide_http/handler
)
main = () {
listener = net.listen("127.0.0.1", 18080)
stdio.print("listening on 127.0.0.1:18080")
task.run(() { http.serve(listener, handler.handle) })
} Start it by hand from guide-http/:
i run --opt 2 listening on 127.0.0.1:18080 This command keeps running. net.listen opens the local TCP listener; http.serve reads HTTP messages and calls handler.handle with the buffered request body. task.run supplies the task runner for the service. The library handles connections inside a structured task scope.
In another terminal, run these commands. The -w option adds a newline and the HTTP status after each body.
curl -sS -w '\n%{http_code}\n' http://127.0.0.1:18080/health {"status":"ok"}
200 curl -sS -w '\n%{http_code}\n' -H 'Content-Type: application/json' \
-d '{"name":"Ada"}' http://127.0.0.1:18080/users {"name":"Ada"}
200 curl -sS -w '\n%{http_code}\n' -H 'Content-Type: application/json' \
-d '{}' http://127.0.0.1:18080/users {"error":"invalid user"}
422 curl -sS -w '\n%{http_code}\n' http://127.0.0.1:18080/missing {"error":"not found"}
404 Stop the server with Ctrl-C when finished. The server and this curl transcript were checked by hand at --opt 0 and --opt 2. The nonterminating server is deliberately a complete file example, not a run fence. The tested handler and the served handler are the same src/handler.i file.
See also
Read IO operations, §9.12 for i/net, and tasks, §9.9 for the server’s task lifetime. Continue with testing for generated tests and fake services.