Current section
Files
Jump to
Current section
Files
src/gleam/http/request.gleam
import gleam/http.{type Header, type Method, type Scheme, Get}
import gleam/http/cookie
import gleam/list
import gleam/option.{type Option}
import gleam/result
import gleam/string
import gleam/uri.{type Uri, Uri}
/// A HTTP request.
///
/// The body of the request is parameterised. The HTTP server or client you are
/// using will have a particular set of types it supports for the body.
///
pub type Request(body) {
Request(
method: Method,
/// The request headers. The keys must always be lowercase.
headers: List(Header),
body: body,
scheme: Scheme,
host: String,
port: Option(Int),
path: String,
query: Option(String),
)
}
/// Return the uri that a request was sent to.
///
pub fn to_uri(request: Request(body)) -> Uri {
Uri(
scheme: option.Some(http.scheme_to_string(request.scheme)),
userinfo: option.None,
host: option.Some(request.host),
port: request.port,
path: request.path,
query: request.query,
fragment: option.None,
)
}
/// Construct a request from a URI.
///
pub fn from_uri(uri: Uri) -> Result(Request(String), Nil) {
use scheme <- result.try(
uri.scheme
|> option.unwrap("")
|> http.scheme_from_string,
)
use host <- result.try(
uri.host
|> option.to_result(Nil),
)
let req =
Request(
method: Get,
headers: [],
body: "",
scheme:,
host:,
port: uri.port,
path: uri.path,
query: uri.query,
)
Ok(req)
}
/// Get the value for a given header.
///
/// If the request does not have that header then `Error(Nil)` is returned.
///
/// Header keys are always lowercase in `gleam_http`. To use any uppercase
/// letter is invalid.
///
pub fn get_header(request: Request(body), key: String) -> Result(String, Nil) {
list.key_find(request.headers, string.lowercase(key))
}
/// Set the header with the given value under the given header key.
///
/// If already present, it is replaced.
///
/// Header keys are always lowercase in `gleam_http`. To use any uppercase
/// letter is invalid.
///
pub fn set_header(
request: Request(body),
key: String,
value: String,
) -> Request(body) {
let headers = list.key_set(request.headers, string.lowercase(key), value)
Request(..request, headers:)
}
/// Prepend the header with the given value under the given header key.
///
/// Similar to `set_header` except if the header already exists it prepends
/// another header with the same key.
///
/// Header keys are always lowercase in `gleam_http`. To use any uppercase
/// letter is invalid.
///
pub fn prepend_header(
request: Request(body),
key: String,
value: String,
) -> Request(body) {
let headers = [#(string.lowercase(key), value), ..request.headers]
Request(..request, headers:)
}
/// Set the body of the request, overwriting any existing body.
///
pub fn set_body(req: Request(old_body), body: new_body) -> Request(new_body) {
Request(..req, body:)
}
/// Update the body of a request using a given function.
///
pub fn map(
request: Request(old_body),
transform: fn(old_body) -> new_body,
) -> Request(new_body) {
request.body
|> transform
|> set_body(request, _)
}
/// Return the non-empty segments of a request path.
///
/// # Examples
///
/// ```gleam
/// > new()
/// > |> set_path("/one/two/three")
/// > |> path_segments
/// ["one", "two", "three"]
/// ```
///
pub fn path_segments(request: Request(body)) -> List(String) {
request.path
|> uri.path_segments
}
/// Decode the query of a request.
pub fn get_query(request: Request(body)) -> Result(List(#(String, String)), Nil) {
case request.query {
option.Some(query_string) -> uri.parse_query(query_string)
option.None -> Ok([])
}
}
/// Set the query of the request.
/// Query params will be percent encoded before being added to the Request.
///
pub fn set_query(
req: Request(body),
query: List(#(String, String)),
) -> Request(body) {
let query =
list.map(query, fn(pair) {
let #(key, value) = pair
uri.percent_encode(key) <> "=" <> uri.percent_encode(value)
})
|> string.join(with: "&")
|> option.Some
Request(..req, query:)
}
/// Set the method of the request.
///
pub fn set_method(req: Request(body), method: Method) -> Request(body) {
Request(..req, method:)
}
/// A request with commonly used default values. This request can be used as
/// an initial value and then update to create the desired request.
///
pub fn new() -> Request(String) {
Request(
method: Get,
headers: [],
body: "",
scheme: http.Https,
host: "localhost",
port: option.None,
path: "",
query: option.None,
)
}
/// Construct a request from a URL string
///
pub fn to(url: String) -> Result(Request(String), Nil) {
url
|> uri.parse
|> result.try(from_uri)
}
/// Set the scheme (protocol) of the request.
///
pub fn set_scheme(req: Request(body), scheme: Scheme) -> Request(body) {
Request(..req, scheme:)
}
/// Set the host of the request.
///
pub fn set_host(req: Request(body), host: String) -> Request(body) {
Request(..req, host:)
}
/// Set the port of the request.
///
pub fn set_port(req: Request(body), port: Int) -> Request(body) {
Request(..req, port: option.Some(port))
}
/// Set the path of the request.
///
pub fn set_path(req: Request(body), path: String) -> Request(body) {
Request(..req, path:)
}
/// Set a cookie on a request, replacing any previous cookie with that name.
///
/// All cookies should be stored in a single header named `cookie`.
/// There should be at most one header with the name `cookie`, otherwise this
/// function cannot guarentee that previous cookies with the same name are
/// replaced.
///
pub fn set_cookie(
req: Request(body),
name: String,
value: String,
) -> Request(body) {
// Get the cookies
let #(cookies, headers) =
list.key_pop(req.headers, "cookie") |> result.unwrap(#("", req.headers))
// Set the new cookie, replacing any previous one with the same name
let cookies =
cookie.parse(cookies)
|> list.key_set(name, value)
|> list.map(fn(pair) { pair.0 <> "=" <> pair.1 })
|> string.join("; ")
Request(..req, headers: [#("cookie", cookies), ..headers])
}
/// Fetch the cookies sent in a request.
///
/// Note badly formed cookie pairs will be ignored.
/// RFC6265 specifies that invalid cookie names/attributes should be ignored.
pub fn get_cookies(req: Request(body)) -> List(#(String, String)) {
let Request(headers:, ..) = req
list.flat_map(headers, fn(header) {
case header {
#("cookie", value) -> cookie.parse(value)
_ -> []
}
})
}
/// Remove a cookie from a request
///
/// Remove a cookie from the request. If no cookie is found return the request
/// unchanged. This will not remove the cookie from the client.
pub fn remove_cookie(req: Request(body), name: String) -> Request(body) {
case list.key_pop(req.headers, "cookie") {
Ok(#(cookies_string, headers)) -> {
let new_cookies_string =
cookie.parse(cookies_string)
|> list.filter_map(fn(cookie) {
case cookie {
#(cookie_name, _) if cookie_name == name -> Error(Nil)
#(name, value) -> Ok(name <> "=" <> value)
}
})
|> string.join("; ")
Request(..req, headers: [#("cookie", new_cookies_string), ..headers])
}
Error(_) -> req
}
}