Module

pooka/http

src/pooka/http.pooka

Requests, 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.

Pooka
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

class

Method

src/pooka/http.pooka:37
class Method

What a request asks for.

class

Get

src/pooka/http.pooka:39
class Get is Method
class

Post

src/pooka/http.pooka:41
class Post is Method
class

Put

src/pooka/http.pooka:43
class Put is Method
class

Patch

src/pooka/http.pooka:45
class Patch is Method
class

Delete

src/pooka/http.pooka:47
class Delete is Method
class

Head

src/pooka/http.pooka:49
class Head is Method
class

Options

src/pooka/http.pooka:51
class Options is Method
class

Trace

src/pooka/http.pooka:53
class Trace is Method
class

Connect

src/pooka/http.pooka:55
class Connect is Method
class

Other

src/pooka/http.pooka:58
class Other(name: String) is Method

Any other method, as it was written, such as PROPFIND.

field

name

src/pooka/http.pooka:58
name: String
INFERREDString
constructor

new

src/pooka/http.pooka:58
INFERREDfn(String) -> pooka/http.Other
function

method_from_string

src/pooka/http.pooka:69
method_from_string(name: String) -> Method
INFERREDfn(String) -> pooka/http.Method

Read a method from its name. Standard methods are matched without regard to case; anything else becomes Other, in uppercase.

Pooka
import pooka/http

assert http.method_from_string("get") == http.Get()
assert http.method_from_string("BREW") == http.Other("BREW")
function

method_to_string

src/pooka/http.pooka:94
method_to_string(method: Method) -> String
INFERREDfn(pooka/http.Method) -> String

A method's name, as it goes on the wire.

function

find_header

src/pooka/http.pooka:112
find_header(headers: List(#(String, String)), name: String) -> Option(String)
INFERREDfn(List(#(String, String)), String) -> Option(String)

The value of the first header with this name, whatever its case.

function

header_values

src/pooka/http.pooka:124
header_values(headers: List(#(String, String)), name: String) -> List(String)
INFERREDfn(List(#(String, String)), String) -> List(String)

Every value of headers with this name, in order. Useful for set-cookie, which may appear many times.

function

set_header

src/pooka/http.pooka:135
set_header(
  headers: List(#(String, String)),
  name: String,
  value: String
) -> List(#(String, String))
INFERREDfn(List(#(String, String)), String, String) -> List(#(String, String))

Replace every header with this name by one with this value.

function

add_header

src/pooka/http.pooka:148
add_header(
  headers: List(#(String, String)),
  name: String,
  value: String
) -> List(#(String, String))
INFERREDfn(List(#(String, String)), String, String) -> List(#(String, String))

Add a header, keeping any others with the same name.

class

Request

src/pooka/http.pooka:164
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.

field

method

src/pooka/http.pooka:165
let method: Method
field

scheme

src/pooka/http.pooka:166
let scheme: String
INFERREDString
field

host

src/pooka/http.pooka:167
let host: String
INFERREDString
field

port

src/pooka/http.pooka:168
let port: Option(Int)
INFERREDOption(Int)
field

path

src/pooka/http.pooka:169
let path: String
INFERREDString
field

query

src/pooka/http.pooka:170
let query: Option(String)
INFERREDOption(String)
field

headers

src/pooka/http.pooka:171
let headers: List(#(String, String))
INFERREDList(#(String, String))
field

body

src/pooka/http.pooka:172
let body: BitArray
INFERREDBitArray
constructor

new

src/pooka/http.pooka:174
new(
    method: Method,
    scheme: String,
    host: String,
    port: Option(Int),
    path: String,
    query: Option(String),
    headers: List(#(String, String)),
    body: BitArray
  )
INFERREDfn(pooka/http.Method, String, String, Option(Int), String, Option(String), List(#(String, String)), BitArray) -> pooka/http.Request
method

header

src/pooka/http.pooka:195
header(name: String) -> Option(String)
INFERREDfn(String) -> Option(String)

The value of a header, whatever case it is asked for in.

method

segments

src/pooka/http.pooka:201
segments() -> List(String)
INFERREDfn() -> List(String)

The path, split at / and decoded, without empty pieces. Match on it to route: /books/7/ gives ["books", "7"].

method

query_params

src/pooka/http.pooka:207
query_params() -> List(#(String, String))
INFERREDfn() -> List(#(String, String))

The query's pairs, decoded, in their written order. A malformed query gives no pairs.

method

checked_query_params

src/pooka/http.pooka:222
checked_query_params() -> Result(List(#(String, String)), Nil)
INFERREDfn() -> Result(List(#(String, String)), Nil)

Read query pairs without mistaking a broken escape for an empty query.

Pooka
assert http.default_request().checked_query_params() == Ok([])
method

checked_segments

src/pooka/http.pooka:234
checked_segments() -> Result(List(String), Nil)
INFERREDfn() -> Result(List(String), Nil)

Decode path pieces, returning the first malformed escape to the caller.

Pooka
assert http.default_request().with_path("/%zz").checked_segments() == Error(Nil)
method

query_param

src/pooka/http.pooka:240
query_param(name: String) -> Option(String)
INFERREDfn(String) -> Option(String)

The first value of a query parameter.

method

cookies

src/pooka/http.pooka:250
cookies() -> List(#(String, String))
INFERREDfn() -> List(#(String, String))

The cookies the client sent, as name and value.

method

cookie

src/pooka/http.pooka:255
cookie(name: String) -> Option(String)
INFERREDfn(String) -> Option(String)

The value of one cookie.

method

text

src/pooka/http.pooka:263
text() -> Result(String, Nil)
INFERREDfn() -> Result(String, Nil)

The body as text, if it is valid UTF-8.

method

validate

src/pooka/http.pooka:277
validate() -> Result(Nil, String)
INFERREDfn() -> 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.

Pooka
let request = http.default_request().with_header("x-note", "owl\r\nforged: yes")
let assert Error(_) = request.validate()
method

url

src/pooka/http.pooka:291
url() -> String
INFERREDfn() -> String

The whole URL, put back together.

method

with_method

src/pooka/http.pooka:310
with_method(method: Method) -> Request
method

with_path

src/pooka/http.pooka:314
with_path(path: String) -> Request
INFERREDfn(String) -> pooka/http.Request
method

with_query

src/pooka/http.pooka:319
with_query(pairs: List(#(String, String))) -> Request
INFERREDfn(List(#(String, String))) -> pooka/http.Request

Set the query from pairs, encoding them.

method

with_header

src/pooka/http.pooka:327
with_header(name: String, value: String) -> Request
INFERREDfn(String, String) -> pooka/http.Request
method

with_body

src/pooka/http.pooka:332
with_body(body: BitArray) -> Request
INFERREDfn(BitArray) -> pooka/http.Request
method

with_text

src/pooka/http.pooka:337
with_text(text: String) -> Request
INFERREDfn(String) -> pooka/http.Request

Set a text body, and its content type if none is set yet.

method

with_json

src/pooka/http.pooka:352
with_json(text: String) -> Request
INFERREDfn(String) -> pooka/http.Request

Set a body of JSON text, already encoded, and its content type.

function

request

src/pooka/http.pooka:377
request(method: Method, url: String) -> Result(Request, String)
INFERREDfn(pooka/http.Method, String) -> Result(pooka/http.Request, String)

A request for a URL, with the given method and no body.

Pooka
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")
function

get

src/pooka/http.pooka:422
get(url: String) -> Result(Request, String)
INFERREDfn(String) -> Result(pooka/http.Request, String)

A GET request for a URL.

function

post

src/pooka/http.pooka:427
post(url: String) -> Result(Request, String)
INFERREDfn(String) -> Result(pooka/http.Request, String)

A POST request for a URL.

function

default_request

src/pooka/http.pooka:438
default_request() -> Request
INFERREDfn() -> pooka/http.Request

A request to http://localhost/, to adjust for tests.

Pooka
import pooka/http

let request = http.default_request().with_path("/owls")
class

Response

src/pooka/http.pooka:445
class Response

A response, as a server sends it or a client receives it.

field

status

src/pooka/http.pooka:446
let status: Int
INFERREDInt
field

headers

src/pooka/http.pooka:447
let headers: List(#(String, String))
INFERREDList(#(String, String))
field

body

src/pooka/http.pooka:448
let body: BitArray
INFERREDBitArray
constructor

new

src/pooka/http.pooka:450
new(status: Int, headers: List(#(String, String)), body: BitArray)
INFERREDfn(Int, List(#(String, String)), BitArray) -> pooka/http.Response
method

header

src/pooka/http.pooka:456
header(name: String) -> Option(String)
INFERREDfn(String) -> Option(String)
method

text

src/pooka/http.pooka:461
text() -> Result(String, Nil)
INFERREDfn() -> Result(String, Nil)

The body as text, if it is valid UTF-8.

method

is_success

src/pooka/http.pooka:469
is_success() -> Bool
INFERREDfn() -> Bool

Is the status between 200 and 299?

method

with_status

src/pooka/http.pooka:473
with_status(status: Int) -> Response
INFERREDfn(Int) -> pooka/http.Response
method

with_header

src/pooka/http.pooka:477
with_header(name: String, value: String) -> Response
INFERREDfn(String, String) -> pooka/http.Response
method

with_body

src/pooka/http.pooka:481
with_body(body: BitArray) -> Response
INFERREDfn(BitArray) -> pooka/http.Response
method

set_cookie

src/pooka/http.pooka:486
set_cookie(cookie: Cookie) -> Response

Add a cookie for the client to keep.

function

response

src/pooka/http.pooka:492
response(status: Int) -> Response
INFERREDfn(Int) -> pooka/http.Response

An empty response.

function

text

src/pooka/http.pooka:504
text(status: Int, body: String) -> Response
INFERREDfn(Int, String) -> pooka/http.Response

A plain text response.

Pooka
import pooka/http

let reply = http.text(200, "hoo")
assert reply.header("Content-Type") == Some("text/plain; charset=utf-8")
function

html

src/pooka/http.pooka:513
html(status: Int, body: String) -> Response
INFERREDfn(Int, String) -> pooka/http.Response

An HTML response.

function

json

src/pooka/http.pooka:522
json(status: Int, body: String) -> Response
INFERREDfn(Int, String) -> pooka/http.Response

A response of JSON text, already encoded, for instance with pooka/json.

function

redirect

src/pooka/http.pooka:532
redirect(location: String) -> Response
INFERREDfn(String) -> pooka/http.Response

Send the client somewhere else. 303 See Other makes the client follow with a GET, which is right after a form has been posted.

function

reason

src/pooka/http.pooka:537
reason(status: Int) -> String
INFERREDfn(Int) -> String

The usual phrase for a status, such as Not Found for 404.

enum

SameSite

src/pooka/http.pooka:573
enum SameSite

How strictly a browser keeps a cookie to its own site.

variant

Strict

src/pooka/http.pooka:574
SameSite.Strict
variant

Lax

src/pooka/http.pooka:575
SameSite.Lax
variant

NoRestriction

src/pooka/http.pooka:576
SameSite.NoRestriction
class

Cookie

src/pooka/http.pooka:587
class Cookie

A cookie to send with a response. Start with cookie, then adjust it.

Pooka
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"
field

name

src/pooka/http.pooka:588
let name: String
INFERREDString
field

value

src/pooka/http.pooka:589
let value: String
INFERREDString
field

path

src/pooka/http.pooka:590
let path: String
INFERREDString
field

domain

src/pooka/http.pooka:591
let domain: Option(String)
INFERREDOption(String)
field

max_age

src/pooka/http.pooka:592
let max_age: Option(Int)
INFERREDOption(Int)
field

http_only

src/pooka/http.pooka:593
let http_only: Bool
INFERREDBool
field

secure

src/pooka/http.pooka:594
let secure: Bool
INFERREDBool
field

same_site

src/pooka/http.pooka:595
let same_site: SameSite
constructor

new

src/pooka/http.pooka:597
new(
    name: String,
    value: String,
    path: String,
    domain: Option(String),
    max_age: Option(Int),
    http_only: Bool,
    secure: Bool,
    same_site: SameSite
  )
INFERREDfn(String, String, String, Option(String), Option(Int), Bool, Bool, pooka/http.SameSite) -> pooka/http.Cookie
method

with_path

src/pooka/http.pooka:617
with_path(path: String) -> Cookie
INFERREDfn(String) -> pooka/http.Cookie
method

with_domain

src/pooka/http.pooka:621
with_domain(domain: String) -> Cookie
INFERREDfn(String) -> pooka/http.Cookie
method

with_max_age

src/pooka/http.pooka:635
with_max_age(seconds: Int) -> Cookie
INFERREDfn(Int) -> pooka/http.Cookie

How many seconds the browser keeps it. Zero removes it.

method

readable_by_scripts

src/pooka/http.pooka:649
readable_by_scripts() -> Cookie
INFERREDfn() -> pooka/http.Cookie

Let page scripts read it. Leave this off for anything secret.

method

allow_insecure

src/pooka/http.pooka:654
allow_insecure() -> Cookie
INFERREDfn() -> pooka/http.Cookie

Send it over plain HTTP too. Useful on localhost, during development.

method

with_same_site

src/pooka/http.pooka:658
with_same_site(same_site: SameSite) -> Cookie
method

validate

src/pooka/http.pooka:668
validate() -> Result(Nil, String)
INFERREDfn() -> Result(Nil, String)

Check names, attributes and browser security rules before rendering.

Pooka
assert http.cookie("session", "abc").validate() == Ok(Nil)
let assert Error(_) = http.cookie("session", "abc; admin=yes").validate()
method

checked_header

src/pooka/http.pooka:684
checked_header() -> Result(String, String)
INFERREDfn() -> Result(String, String)

Render a cookie only when its attributes are safe to send.

Pooka
let assert Error(_) = http.cookie("bad name", "value").checked_header()
method

header

src/pooka/http.pooka:691
header() -> String
INFERREDfn() -> String

The cookie as the value of a set-cookie header. For values supplied by a caller, use checked_header first.

function

cookie

src/pooka/http.pooka:708
cookie(name: String, value: String) -> Cookie
INFERREDfn(String, String) -> pooka/http.Cookie

A 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.

function

expire_cookie

src/pooka/http.pooka:713
expire_cookie(name: String) -> Cookie
INFERREDfn(String) -> pooka/http.Cookie

A cookie that tells the browser to forget an earlier one.

function

parse_query

src/pooka/http.pooka:776
parse_query(query: String) -> Result(List(#(String, String)), Nil)
INFERREDfn(String) -> Result(List(#(String, String)), Nil)

Decode a query string into pairs, in order.

Pooka
import pooka/http

assert http.parse_query("q=owl+%26+moss&page=2") == Ok([#("q", "owl & moss"), #("page", "2")])
function

encode_query

src/pooka/http.pooka:790
encode_query(pairs: List(#(String, String))) -> String
INFERREDfn(List(#(String, String))) -> String

Encode pairs as a query string.

Pooka
import pooka/http

assert http.encode_query([#("q", "owl & moss")]) == "q=owl+%26+moss"
function

percent_encode

src/pooka/http.pooka:796
percent_encode(text: String) -> String
INFERREDfn(String) -> String

Encode text for one piece of a path: everything but letters, digits and -._~ is written as %XX.

function

percent_decode

src/pooka/http.pooka:801
percent_decode(text: String) -> Result(String, Nil)
INFERREDfn(String) -> Result(String, Nil)

Decode %XX sequences. Fails if the result is not valid UTF-8.