Module
pooka/http
src/pooka/http.pookaRequests, responses and the words they share.
This module has no server and no client in it. It is the vocabulary that servers, clients and frameworks agree on, so a handler written for one server can be tested without any server at all, and a request built for a client looks like the one a server receives.
import pooka/http.{Request, Response}
handle(request: Request) -> Response {
match #(request.method, request.segments()) {
#(http.Get, []) -> http.text(200, "Welcome to the night library")
#(http.Get, ["books", id]) -> http.text(200, "Book " + id)
_ -> http.text(404, "Nothing on this shelf")
}
}
Header names are kept in lowercase, the way HTTP/2 writes them, so looking one up never depends on how the other side spelled it.
API reference
- classMethod
- classGet
- classPost
- classPut
- classPatch
- classDelete
- classHead
- classOptions
- classTrace
- classConnect
- classOther
- functionmethod_from_string
- functionmethod_to_string
- functionfind_header
- functionheader_values
- functionset_header
- functionadd_header
- classRequest
- functionrequest
- functionget
- functionpost
- functiondefault_request
- classResponse
- functionresponse
- functiontext
- functionhtml
- functionjson
- functionredirect
- functionreason
- enumSameSite
- classCookie
- functioncookie
- functionexpire_cookie
- functionparse_query
- functionencode_query
- functionpercent_encode
- functionpercent_decode
class Method
What a request asks for.
class Get is Method
class Post is Method
class Put is Method
class Patch is Method
class Delete is Method
class Head is Method
class Options is Method
class Trace is Method
class Connect is Method
class Other(name: String) is Method
Any other method, as it was written, such as PROPFIND.
name: String
String
fn(String) -> pooka/http.Othermethod_from_string(name: String) -> Method
fn(String) -> pooka/http.MethodRead a method from its name. Standard methods are matched without regard
to case; anything else becomes Other, in uppercase.
import pooka/http
assert http.method_from_string("get") == http.Get()
assert http.method_from_string("BREW") == http.Other("BREW")
method_to_string(method: Method) -> String
fn(pooka/http.Method) -> StringA method's name, as it goes on the wire.
find_header(headers: List(#(String, String)), name: String) -> Option(String)
fn(List(#(String, String)), String) -> Option(String)The value of the first header with this name, whatever its case.
header_values(headers: List(#(String, String)), name: String) -> List(String)
fn(List(#(String, String)), String) -> List(String)Every value of headers with this name, in order. Useful for set-cookie,
which may appear many times.
set_header(
headers: List(#(String, String)),
name: String,
value: String
) -> List(#(String, String))
fn(List(#(String, String)), String, String) -> List(#(String, String))Replace every header with this name by one with this value.
add_header(
headers: List(#(String, String)),
name: String,
value: String
) -> List(#(String, String))
fn(List(#(String, String)), String, String) -> List(#(String, String))Add a header, keeping any others with the same name.
class Request
A request, as a client sends it or a server receives it.
path is percent-encoded, as it was on the wire; segments decodes it.
query is the part after ?, also still encoded; query_params decodes
it. port is None when it is the scheme's usual one. The body is bytes;
text reads it as UTF-8.
let method: Method
pooka/http.Methodlet scheme: String
Stringlet host: String
Stringlet port: Option(Int)
Option(Int)let path: String
Stringlet query: Option(String)
Option(String)let headers: List(#(String, String))
List(#(String, String))let body: BitArray
BitArraynew(
method: Method,
scheme: String,
host: String,
port: Option(Int),
path: String,
query: Option(String),
headers: List(#(String, String)),
body: BitArray
)
fn(pooka/http.Method, String, String, Option(Int), String, Option(String), List(#(String, String)), BitArray) -> pooka/http.Requestheader(name: String) -> Option(String)
fn(String) -> Option(String)The value of a header, whatever case it is asked for in.
segments() -> List(String)
fn() -> List(String)The path, split at / and decoded, without empty pieces. Match on it to
route: /books/7/ gives ["books", "7"].
query_params() -> List(#(String, String))
fn() -> List(#(String, String))The query's pairs, decoded, in their written order. A malformed query gives no pairs.
checked_query_params() -> Result(List(#(String, String)), Nil)
fn() -> Result(List(#(String, String)), Nil)Read query pairs without mistaking a broken escape for an empty query.
assert http.default_request().checked_query_params() == Ok([])
checked_segments() -> Result(List(String), Nil)
fn() -> Result(List(String), Nil)Decode path pieces, returning the first malformed escape to the caller.
assert http.default_request().with_path("/%zz").checked_segments() == Error(Nil)
query_param(name: String) -> Option(String)
fn(String) -> Option(String)The first value of a query parameter.
cookies() -> List(#(String, String))
fn() -> List(#(String, String))The cookies the client sent, as name and value.
cookie(name: String) -> Option(String)
fn(String) -> Option(String)The value of one cookie.
text() -> Result(String, Nil)
fn() -> Result(String, Nil)The body as text, if it is valid UTF-8.
validate() -> Result(Nil, String)
fn() -> Result(Nil, String)Check the parts that will become an HTTP message before opening a socket. A newline is useful in a letter, but has no business inside a field value.
let request = http.default_request().with_header("x-note", "owl\r\nforged: yes")
let assert Error(_) = request.validate()
url() -> String
fn() -> StringThe whole URL, put back together.
with_method(method: Method) -> Request
fn(pooka/http.Method) -> pooka/http.Requestwith_path(path: String) -> Request
fn(String) -> pooka/http.Requestwith_query(pairs: List(#(String, String))) -> Request
fn(List(#(String, String))) -> pooka/http.RequestSet the query from pairs, encoding them.
with_header(name: String, value: String) -> Request
fn(String, String) -> pooka/http.Requestwith_body(body: BitArray) -> Request
fn(BitArray) -> pooka/http.Requestwith_text(text: String) -> Request
fn(String) -> pooka/http.RequestSet a text body, and its content type if none is set yet.
with_json(text: String) -> Request
fn(String) -> pooka/http.RequestSet a body of JSON text, already encoded, and its content type.
request(method: Method, url: String) -> Result(Request, String)
fn(pooka/http.Method, String) -> Result(pooka/http.Request, String)A request for a URL, with the given method and no body.
import pooka/http
let assert Ok(request) = http.request(http.Get(), "https://example.org/owls?age=3")
assert request.host == "example.org"
assert request.segments() == ["owls"]
assert request.query_param("age") == Some("3")
get(url: String) -> Result(Request, String)
fn(String) -> Result(pooka/http.Request, String)A GET request for a URL.
post(url: String) -> Result(Request, String)
fn(String) -> Result(pooka/http.Request, String)A POST request for a URL.
default_request() -> Request
fn() -> pooka/http.RequestA request to http://localhost/, to adjust for tests.
import pooka/http
let request = http.default_request().with_path("/owls")
class Response
A response, as a server sends it or a client receives it.
let status: Int
Intlet headers: List(#(String, String))
List(#(String, String))let body: BitArray
BitArraynew(status: Int, headers: List(#(String, String)), body: BitArray)
fn(Int, List(#(String, String)), BitArray) -> pooka/http.Responseheader(name: String) -> Option(String)
fn(String) -> Option(String)text() -> Result(String, Nil)
fn() -> Result(String, Nil)The body as text, if it is valid UTF-8.
is_success() -> Bool
fn() -> BoolIs the status between 200 and 299?
with_status(status: Int) -> Response
fn(Int) -> pooka/http.Responsewith_header(name: String, value: String) -> Response
fn(String, String) -> pooka/http.Responsewith_body(body: BitArray) -> Response
fn(BitArray) -> pooka/http.Responseset_cookie(cookie: Cookie) -> Response
fn(pooka/http.Cookie) -> pooka/http.ResponseAdd a cookie for the client to keep.
response(status: Int) -> Response
fn(Int) -> pooka/http.ResponseAn empty response.
text(status: Int, body: String) -> Response
fn(Int, String) -> pooka/http.ResponseA plain text response.
import pooka/http
let reply = http.text(200, "hoo")
assert reply.header("Content-Type") == Some("text/plain; charset=utf-8")
html(status: Int, body: String) -> Response
fn(Int, String) -> pooka/http.ResponseAn HTML response.
json(status: Int, body: String) -> Response
fn(Int, String) -> pooka/http.ResponseA response of JSON text, already encoded, for instance with pooka/json.
redirect(location: String) -> Response
fn(String) -> pooka/http.ResponseSend the client somewhere else. 303 See Other makes the client follow
with a GET, which is right after a form has been posted.
reason(status: Int) -> String
fn(Int) -> StringThe usual phrase for a status, such as Not Found for 404.
enum SameSite
How strictly a browser keeps a cookie to its own site.
SameSite.Strict
SameSite.Lax
SameSite.NoRestriction
class Cookie
A cookie to send with a response. Start with cookie, then adjust it.
import pooka/http
let session = http.cookie("session", "abc123").with_max_age(3600)
assert session.header() == "session=abc123; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax"
let name: String
Stringlet value: String
Stringlet path: String
Stringlet domain: Option(String)
Option(String)let max_age: Option(Int)
Option(Int)let http_only: Bool
Boollet secure: Bool
Boollet same_site: SameSite
pooka/http.SameSitenew(
name: String,
value: String,
path: String,
domain: Option(String),
max_age: Option(Int),
http_only: Bool,
secure: Bool,
same_site: SameSite
)
fn(String, String, String, Option(String), Option(Int), Bool, Bool, pooka/http.SameSite) -> pooka/http.Cookiewith_path(path: String) -> Cookie
fn(String) -> pooka/http.Cookiewith_domain(domain: String) -> Cookie
fn(String) -> pooka/http.Cookiewith_max_age(seconds: Int) -> Cookie
fn(Int) -> pooka/http.CookieHow many seconds the browser keeps it. Zero removes it.
readable_by_scripts() -> Cookie
fn() -> pooka/http.CookieLet page scripts read it. Leave this off for anything secret.
allow_insecure() -> Cookie
fn() -> pooka/http.CookieSend it over plain HTTP too. Useful on localhost, during development.
with_same_site(same_site: SameSite) -> Cookie
fn(pooka/http.SameSite) -> pooka/http.Cookievalidate() -> Result(Nil, String)
fn() -> Result(Nil, String)Check names, attributes and browser security rules before rendering.
assert http.cookie("session", "abc").validate() == Ok(Nil)
let assert Error(_) = http.cookie("session", "abc; admin=yes").validate()
checked_header() -> Result(String, String)
fn() -> Result(String, String)Render a cookie only when its attributes are safe to send.
let assert Error(_) = http.cookie("bad name", "value").checked_header()
header() -> String
fn() -> StringThe cookie as the value of a set-cookie header.
For values supplied by a caller, use checked_header first.
cookie(name: String, value: String) -> Cookie
fn(String, String) -> pooka/http.CookieA cookie for the whole site, hidden from page scripts, sent only over HTTPS, and kept to this site for anything but plain links: the safe defaults for a session.
expire_cookie(name: String) -> Cookie
fn(String) -> pooka/http.CookieA cookie that tells the browser to forget an earlier one.
parse_query(query: String) -> Result(List(#(String, String)), Nil)
fn(String) -> Result(List(#(String, String)), Nil)Decode a query string into pairs, in order.
import pooka/http
assert http.parse_query("q=owl+%26+moss&page=2") == Ok([#("q", "owl & moss"), #("page", "2")])
encode_query(pairs: List(#(String, String))) -> String
fn(List(#(String, String))) -> StringEncode pairs as a query string.
import pooka/http
assert http.encode_query([#("q", "owl & moss")]) == "q=owl+%26+moss"
percent_encode(text: String) -> String
fn(String) -> StringEncode text for one piece of a path: everything but letters, digits and
-._~ is written as %XX.
percent_decode(text: String) -> Result(String, Nil)
fn(String) -> Result(String, Nil)Decode %XX sequences. Fails if the result is not valid UTF-8.