Skip to main content

src/aws/internal/http_request.gleam

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")
}