March Docs

Http

Http module: pure HTTP protocol types, constructors, and transforms.

This is Layer 1 of March's HTTP library. It contains no I/O — only data types and functions for building and inspecting HTTP requests and responses. Libraries that work with HTTP types depend on this alone.

Types

typeMethodMethod = Get | Post | Put | Patch | Delete | Head | Options | Trace | Connect | Other(String)#

An HTTP request method. The nine standard methods are nullary constructors; Other(name) carries any extension method (PROPFIND, MKCOL, ...) verbatim. Render one for the wire with method_to_string.

typeSchemeScheme = SchemeHttp | SchemeHttps#

The URL scheme a request targets: SchemeHttp (http://) or SchemeHttps (https://).

typeStatusStatus = Status(Int)#

An HTTP status code. Status(n) wraps any integer, so non-standard codes such as Status(418) are representable. Read it with status_code, build the common ones with status_ok, status_not_found, ...; classify with is_success, is_redirect, is_client_error, ...

typeHeaderHeader = Header(String, String)#

One header field: Header(name, value). Names keep the case they were created with; lookups (get_header, get_request_header) compare names case-insensitively. Header lists are plain List(Header) and may hold the same name more than once.

typeUrlErrorUrlError = InvalidScheme(String) | MissingHost | InvalidPort(String) | MalformedUrl(String)#

Why parse_url rejected a URL:

  • InvalidScheme(url): the URL starts with neither http:// nor https://.
  • MissingHost: nothing between the scheme and the first /, : or end.
  • InvalidPort(host_and_port): the text after : is not an integer.
  • MalformedUrl(url): reserved for other malformed input; parse_url does not currently produce it.
typeRequestRequest(body) = Request(Method, Scheme, String, Option(Int), String, Option(String), List(Header), body)#

An HTTP request, generic over its body type: Request(method, scheme, host, port, path, query, headers, body).

port is None when the URL named none (use the scheme's default), query is the raw text after ? without the ?, and body is whatever the caller stores there: Unit for a request built by get/parse_url, a String or Bytes for one built by post. Prefer the accessors (method, host, path, ...) and the set_* transforms over matching on the constructor: its field order is an implementation detail.

typeResponseResponse(body) = Response(Status, List(Header), body)#

An HTTP response, generic over its body type: Response(status, headers, body). Read it with response_status, response_status_code, response_headers, response_body, and get_header.

Functions

fnbodybody(req : Request(b)) : b#

The request body: () for a request built by get/parse_url.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.body(req)
()
march> match Http.post("http://example.com/api", "{}") do Ok(r) -> Http.body(r) | Err(_) -> "" end
"{}"
fndeletedelete(url : String) : Result(Request(Unit), UrlError)#

A DELETE request for url, with a Unit body. Fails exactly when parse_url(url) does.

Examples

march> match Http.delete("http://example.com/items/1") do Ok(r) -> Http.method_to_string(Http.method(r)) | Err(_) -> "" end
"DELETE"
march> Http.delete("http://example.com:x/") == Err(InvalidPort("example.com:x"))
true
fnencode_queryencode_query(params : List((String, String))) : String#

Join (key, value) pairs into a query string k1=v1&k2=v2, in order.

Keys and values are inserted VERBATIM: nothing is percent-encoded, so a value containing &, =, #, a space or non-ASCII text produces a different query than intended. Use Uri.encode_query when the pairs are not already encoded.

Examples

march> Http.encode_query([("q", "march"), ("page", "2")])
"q=march&page=2"
march> Http.encode_query([])
""
fngetget(url : String) : Result(Request(Unit), UrlError)#

A GET request for url. The same as parse_url, named for symmetry with post, put, ...

Examples

march> match Http.get("http://example.com/x") do Ok(r) -> Http.method_to_string(Http.method(r)) | Err(_) -> "" end
"GET"
march> Http.get("example.com") == Err(InvalidScheme("example.com"))
true
fnget_headerget_header(resp : Response(b), name : String) : Option(String)#

The value of the first response header named name, compared case-insensitively, or None. When a header repeats, only the first is returned; scan response_headers to see them all.

Examples

march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.get_header(resp, "LOCATION")
Some("/new")
march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.get_header(resp, "content-type")
None
march> Http.get_header(Response(Status(200), [Header("A", "1"), Header("a", "2")], ()), "a")
Some("1")
fnget_request_headerget_request_header(req : Request(b), name : String) : Option(String)#

The value of the first request header named name, compared case-insensitively, or None. set_header prepends, so after setting the same name twice this returns the most recent value.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.get_request_header(Http.set_header(Http.set_header(req, "X-Id", "1"), "x-id", "2"), "X-ID")
Some("2")
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.get_request_header(req, "Accept")
None
fnheadhead(url : String) : Result(Request(Unit), UrlError)#

A HEAD request for url, with a Unit body. Fails exactly when parse_url(url) does.

Examples

march> match Http.head("http://example.com/") do Ok(r) -> Http.method_to_string(Http.method(r)) | Err(_) -> "" end
"HEAD"
fnheadersheaders(req : Request(b)) : List(Header)#

All request headers, most recently added first (set_header prepends). A request built by parse_url/get has none.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.headers(req)
[]
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.headers(Http.set_header(Http.set_header(req, "Accept", "*/*"), "X-Id", "7"))
[Header("X-Id", "7"), Header("Accept", "*/*")]
fnhosthost(req : Request(b)) : String#

The host name, without scheme, port or path.

Examples

march> match Http.parse_url("https://api.example.com:8443/v1/users?page=2") do Ok(r) -> Http.host(r) | Err(_) -> "" end
"api.example.com"
fnis_client_erroris_client_error(s : Status) : Bool#

True for client-error 4xx statuses (caller's fault).

Examples

march> Http.is_client_error(Http.status_not_found())
true
march> Http.is_client_error(Http.status_server_error())
false
fnis_informationalis_informational(s : Status) : Bool#

True for informational 1xx statuses.

Examples

march> Http.is_informational(Status(100))
true
march> Http.is_informational(Status(200))
false
fnis_redirectis_redirect(s : Status) : Bool#

True for redirect 3xx statuses.

Examples

march> Http.is_redirect(Http.status_moved())
true
march> Http.is_redirect(Http.status_ok())
false
fnis_server_erroris_server_error(s : Status) : Bool#

True for server-error 5xx statuses (server's fault).

Examples

march> Http.is_server_error(Http.status_server_error())
true
march> Http.is_server_error(Http.status_not_found())
false
fnis_successis_success(s : Status) : Bool#

True for success 2xx statuses.

Examples

march> Http.is_success(Http.status_ok())
true
march> Http.is_success(Http.status_not_found())
false
fnmethodmethod(req : Request(b)) : Method#

The request method.

Examples

march> match Http.parse_url("https://api.example.com:8443/v1/users?page=2") do Ok(r) -> Http.method_to_string(Http.method(r)) | Err(_) -> "" end
"GET"
fnmethod_to_stringmethod_to_string(m : Method) : String#

Render an HTTP method as its uppercase wire-format string. Other(s) returns its inner string verbatim — useful for non-standard extensions like MKCOL or PROPFIND (WebDAV) which an upstream parser may have wrapped in Other(...).

Examples

march> Http.method_to_string(Get)
"GET"
march> Http.method_to_string(Patch)
"PATCH"
march> Http.method_to_string(Other("MKCOL"))
"MKCOL"
fnoptionsoptions(url : String) : Result(Request(Unit), UrlError)#

An OPTIONS request for url, with a Unit body. Fails exactly when parse_url(url) does.

Examples

march> match Http.options("http://example.com/") do Ok(r) -> Http.method_to_string(Http.method(r)) | Err(_) -> "" end
"OPTIONS"
fnparse_urlparse_url(url : String) : Result(Request(Unit), UrlError)#

Parse an absolute http:// or https:// URL into a GET request with a Unit body and no headers.

The URL is split into host, optional :port, path (default "/") and the raw query after ?. Nothing is percent-decoded and nothing past the split is validated: a #fragment stays part of the path or query, user:pass@ is not recognised, and an IPv6 literal ([::1]) is not supported. The port must be an integer but is not range-checked.

Errors: InvalidScheme for any other scheme, MissingHost for an empty host, InvalidPort for a non-numeric port.

Examples

march> match Http.parse_url("https://api.example.com:8443/v1/users?page=2") do Ok(r) -> Http.host(r) | Err(_) -> "" end
"api.example.com"
march> match Http.parse_url("https://api.example.com:8443/v1/users?page=2") do Ok(r) -> Http.query(r) == Some("page=2") | Err(_) -> false end
true
march> Http.parse_url("ftp://example.com") == Err(InvalidScheme("ftp://example.com"))
true
fnpatchpatch(url : String, bdy : b) : Result(Request(b), UrlError)#

A PATCH request for url carrying bdy. Fails exactly when parse_url(url) does.

Examples

march> match Http.patch("http://example.com/items/1", "data") do Ok(r) -> (Http.method_to_string(Http.method(r)), Http.body(r)) | Err(_) -> ("", "") end
("PATCH", "data")
fnpathpath(req : Request(b)) : String#

The path, starting with /; "/" when the URL had none. Excludes the query string.

Examples

march> match Http.parse_url("https://api.example.com:8443/v1/users?page=2") do Ok(r) -> Http.path(r) | Err(_) -> "" end
"/v1/users"
fnportport(req : Request(b)) : Option(Int)#

The explicit port, or None when the URL named none (the scheme default, 80 or 443, applies).

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.port(req)
Some(8080)
march> match Http.get("https://example.com/") do Ok(r) -> Http.port(r) == None | Err(_) -> false end
true
fnpostpost(url : String, bdy : b) : Result(Request(b), UrlError)#

A POST request for url carrying bdy. Fails exactly when parse_url(url) does. No Content-Type header is added; set one with set_header.

Examples

march> match Http.post("https://example.com/api", "{}") do Ok(r) -> (Http.method_to_string(Http.method(r)), Http.body(r), Http.headers(r)) | Err(_) -> ("", "", []) end
("POST", "{}", [])
march> match Http.post("https://:443/api", "{}") do Ok(_) -> "ok" | Err(MissingHost) -> "missing host" | Err(_) -> "other" end
"missing host"
fnputput(url : String, bdy : b) : Result(Request(b), UrlError)#

A PUT request for url carrying bdy. Fails exactly when parse_url(url) does.

Examples

march> match Http.put("http://example.com/items/1", "data") do Ok(r) -> (Http.method_to_string(Http.method(r)), Http.body(r)) | Err(_) -> ("", "") end
("PUT", "data")
fnqueryquery(req : Request(b)) : Option(String)#

The raw query string after ? (without the ?), or None when the URL had none. Not decoded.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.query(req)
Some("x=1")
march> match Http.get("http://example.com/a%20b?q=a%20b") do Ok(r) -> Http.query(r) == Some("q=a%20b") | Err(_) -> false end
true
fnresponse_bodyresponse_body(resp : Response(b)) : b#

The response body.

Examples

march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.response_body(resp)
"moved"
fnresponse_headersresponse_headers(resp : Response(b)) : List(Header)#

All response headers, in the order they were stored.

Examples

march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.response_headers(resp)
[Header("Location", "/new")]
fnresponse_is_redirectresponse_is_redirect(resp : Response(b)) : Bool#

True when the response status is 3xx. Read the target with get_header(resp, "location").

Examples

march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.response_is_redirect(resp)
true
march> Http.response_is_redirect(Response(Status(200), [], ()))
false
fnresponse_is_successresponse_is_success(resp : Response(b)) : Bool#

True when the response status is 2xx.

Examples

march> Http.response_is_success(Response(Status(204), [], ()))
true
march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.response_is_success(resp)
false
fnresponse_statusresponse_status(resp : Response(b)) : Status#

The response status.

Examples

march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.response_status(resp)
Status(301)
fnresponse_status_coderesponse_status_code(resp : Response(b)) : Int#

The response status as an integer, e.g. 200. Shorthand for status_code(response_status(resp)).

Examples

march> let resp = Response(Status(301), [Header("Location", "/new")], "moved")
march> Http.response_status_code(resp)
301
fnschemescheme(req : Request(b)) : Scheme#

The request scheme, SchemeHttp or SchemeHttps.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.scheme(req)
SchemeHttp
march> match Http.get("https://example.com/") do Ok(r) -> Http.scheme(r) == SchemeHttps | Err(_) -> false end
true
fnset_bodyset_body(req : Request(a), new_body : b) : Request(b)#

Replace the body. The body type may change: a Request(Unit) from get becomes a Request(String) after set_body(req, "...").

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.body(Http.set_body(req, "payload"))
"payload"
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.body(Http.set_body(req, [1, 2, 3]))
[1, 2, 3]
fnset_headerset_header(req : Request(b), name : String, value : String) : Request(b)#

Prepend a header to req. The name and value are sanitized: NUL, CR, and LF bytes are removed to block CRLF-injection attacks even when the inputs come from untrusted sources (a name or value may become empty). An existing header of the same name is not removed; the new one shadows it for get_request_header.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.get_request_header(Http.set_header(req, "Accept", "text/html"), "accept")
Some("text/html")
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.headers(Http.set_header(req, "X-Evil", "a\r\nSet-Cookie: x"))
[Header("X-Evil", "aSet-Cookie: x")]
fnset_hostset_host(req : Request(b), new_host : String) : Request(b)#

Replace the host name. The value is stored verbatim (not validated).

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.host(Http.set_host(req, "api.example.org"))
"api.example.org"
fnset_methodset_method(req : Request(b), m : Method) : Request(b)#

Replace the request method, keeping every other field.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.method_to_string(Http.method(Http.set_method(req, Delete)))
"DELETE"
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.method_to_string(Http.method(Http.set_method(req, Other("PROPFIND"))))
"PROPFIND"
fnset_pathset_path(req : Request(b), new_path : String) : Request(b)#

Replace the path. The query string is separate and is left unchanged; pass a path beginning with /.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.path(Http.set_path(req, "/v2/items"))
"/v2/items"
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.query(Http.set_path(req, "/v2/items"))
Some("x=1")
fnset_portset_port(req : Request(b), new_port : Int) : Request(b)#

Set an explicit port (stored as Some(new_port)). The value is not range-checked.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.port(Http.set_port(req, 443))
Some(443)
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.port(Http.set_port(req, 70000))
Some(70000)
fnset_queryset_query(req : Request(b), params : List((String, String))) : Request(b)#

Replace the query string with encode_query(params); an empty list removes it (query becomes None). Like encode_query, this does not percent-encode.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.query(Http.set_query(req, [("page", "3"), ("sort", "asc")]))
Some("page=3&sort=asc")
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.query(Http.set_query(req, []))
None
fnset_schemeset_scheme(req : Request(b), s : Scheme) : Request(b)#

Replace the request scheme. The port is left alone, so switching to SchemeHttps does not change an explicit :80.

Examples

march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.scheme(Http.set_scheme(req, SchemeHttps))
SchemeHttps
march> let req = Result.unwrap(Http.get("http://example.com:8080/a/b?x=1"))
march> Http.port(Http.set_scheme(req, SchemeHttps))
Some(8080)
fnstatus_bad_requeststatus_bad_request() : Status do Status(400) end#

Status 400 Bad Request.

Examples

march> Http.status_code(Http.status_bad_request())
400
fnstatus_codestatus_code(s : Status) : Int#

Extract the integer status code from a Status.

Examples

march> Http.status_code(Http.status_ok())
200
march> Http.status_code(Status(418))
418
fnstatus_createdstatus_created() : Status do Status(201) end#

Status 201 Created.

Examples

march> Http.status_code(Http.status_created())
201
fnstatus_forbiddenstatus_forbidden() : Status do Status(403) end#

Status 403 Forbidden.

Examples

march> Http.status_code(Http.status_forbidden())
403
fnstatus_foundstatus_found() : Status do Status(302) end#

Status 302 Found.

Examples

march> Http.status_code(Http.status_found())
302
fnstatus_movedstatus_moved() : Status do Status(301) end#

Status 301 Moved Permanently.

Examples

march> Http.status_code(Http.status_moved())
301
fnstatus_no_contentstatus_no_content() : Status do Status(204) end#

Status 204 No Content.

Examples

march> Http.status_code(Http.status_no_content())
204
fnstatus_not_foundstatus_not_found() : Status do Status(404) end#

Status 404 Not Found.

Examples

march> Http.status_code(Http.status_not_found())
404
fnstatus_okstatus_ok() : Status do Status(200) end#

Status 200 OK.

Examples

march> Http.status_code(Http.status_ok())
200
fnstatus_server_errorstatus_server_error() : Status do Status(500) end#

Status 500 Internal Server Error.

Examples

march> Http.status_code(Http.status_server_error())
500
fnstatus_unauthorizedstatus_unauthorized() : Status do Status(401) end#

Status 401 Unauthorized.

Examples

march> Http.status_code(Http.status_unauthorized())
401