import gleam/bit_array
import gleam/list
import gleam/result
import gleam/string
pub type HttpRequest {
HttpRequest(
method: String,
path: String,
query: String,
headers: List(Header),
body: BitArray,
)
}
pub type Header {
Header(name: String, value: String)
}
pub type ParseError {
MalformedRequestLine(String)
MalformedHeader(String)
}
pub fn parse(text: String) -> Result(HttpRequest, ParseError) {
let #(head, body) = split_head_body(text)
case string.split(head, "\n") {
[] -> Error(MalformedRequestLine(""))
[request_line, ..header_lines] -> {
use #(method, path, query) <- result.try(parse_request_line(request_line))
use headers <- result.try(parse_headers(header_lines))
Ok(HttpRequest(
method: method,
path: path,
query: query,
headers: headers,
body: bit_array.from_string(body),
))
}
}
}
fn split_head_body(text: String) -> #(String, String) {
let normalized = string.replace(text, "\r\n", "\n")
case string.split_once(normalized, "\n\n") {
Ok(#(head, body)) -> #(head, body)
// No blank-line separator: treat the whole thing as the head and
// drop any trailing newline so the headers list has no empty tail.
Error(_) -> #(string.trim_end(normalized), "")
}
}
fn parse_request_line(
line: String,
) -> Result(#(String, String, String), ParseError) {
// Method is before the first space, HTTP version after the last. The
// request-target sits between, and is allowed to contain spaces (some
// SigV4 vectors have literal spaces in the path).
case string.split(line, " ") {
[method, ..rest] ->
case list.reverse(rest) {
[_version, ..target_parts_rev] -> {
let target = target_parts_rev |> list.reverse |> string.join(" ")
let #(path, query) = case string.split_once(target, "?") {
Ok(#(p, q)) -> #(p, q)
Error(_) -> #(target, "")
}
Ok(#(method, path, query))
}
[] -> Error(MalformedRequestLine(line))
}
[] -> Error(MalformedRequestLine(line))
}
}
fn parse_headers(lines: List(String)) -> Result(List(Header), ParseError) {
do_parse_headers(lines, [])
}
fn do_parse_headers(
lines: List(String),
acc: List(Header),
) -> Result(List(Header), ParseError) {
case lines {
[] -> Ok(list.reverse(acc))
[line, ..rest] ->
case is_continuation(line), acc {
True, [Header(name, value), ..tail] -> {
let merged =
Header(name: name, value: value <> " " <> string.trim(line))
do_parse_headers(rest, [merged, ..tail])
}
True, [] -> Error(MalformedHeader(line))
False, _ ->
case string.split_once(line, ":") {
Ok(#(name, value)) ->
do_parse_headers(rest, [Header(name: name, value: value), ..acc])
Error(_) -> Error(MalformedHeader(line))
}
}
}
}
fn is_continuation(line: String) -> Bool {
string.starts_with(line, " ") || string.starts_with(line, "\t")
}