A cutesy HTTP client for Gleam

feat: partially working http client

+739
+189
src/requwu.gleam
··· 1 + import gleam/bit_array 2 + import gleam/function 3 + import gleam/http 4 + import gleam/http/response 5 + import gleam/json 6 + import gleam/list 7 + import gleam/option 8 + import gleam/result 9 + import gleam/uri 10 + import mug 11 + import requwu/errors 12 + import requwu/internal/http_codec 13 + import requwu/request.{ 14 + type CompleteRequest, type RequestBuilder, CompleteRequest, 15 + } as requwu_request 16 + 17 + // TODO: detect cyclic redirection 18 + 19 + pub fn request(method method: http.Method, to uri: String) -> RequestBuilder { 20 + requwu_request.to(method, uri) 21 + } 22 + 23 + pub const get: fn(String) -> RequestBuilder = requwu_request.get 24 + 25 + pub const head: fn(String) -> RequestBuilder = requwu_request.head 26 + 27 + pub const post: fn(String) -> RequestBuilder = requwu_request.post 28 + 29 + pub const put: fn(String) -> RequestBuilder = requwu_request.put 30 + 31 + pub const delete: fn(String) -> RequestBuilder = requwu_request.delete 32 + 33 + pub const connect: fn(String) -> RequestBuilder = requwu_request.connect 34 + 35 + pub const options: fn(String) -> RequestBuilder = requwu_request.options 36 + 37 + pub const trace: fn(String) -> RequestBuilder = requwu_request.trace 38 + 39 + pub const header: fn(RequestBuilder, String, String) -> RequestBuilder = requwu_request.header 40 + 41 + pub const headers: fn(RequestBuilder, List(#(String, String))) -> RequestBuilder = requwu_request.headers 42 + 43 + pub const body: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.body 44 + 45 + pub const json: fn(RequestBuilder, json.Json) -> RequestBuilder = requwu_request.json 46 + 47 + pub const basic_auth: fn(RequestBuilder, String, option.Option(String)) -> 48 + RequestBuilder = requwu_request.basic_auth 49 + 50 + pub const bearer_auth: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.bearer_auth 51 + 52 + pub const auth: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.auth 53 + 54 + pub const user_agent: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.user_agent 55 + 56 + pub const max_redirects: fn(RequestBuilder, Int) -> RequestBuilder = requwu_request.max_redirects 57 + 58 + pub const timeout: fn(RequestBuilder, Int) -> RequestBuilder = requwu_request.timeout 59 + 60 + pub const query: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.query 61 + 62 + pub fn send( 63 + builder_request: RequestBuilder, 64 + ) -> Result(response.Response(String), errors.SendError) { 65 + use complete_request <- result.try( 66 + requwu_request.complete(builder_request) 67 + |> result.map_error(errors.EncodeRequestError), 68 + ) 69 + use socket <- result.try(new_socket(complete_request)) 70 + 71 + internal_request(socket, complete_request, 0) 72 + } 73 + 74 + fn new_socket( 75 + complete_request: CompleteRequest, 76 + ) -> Result(mug.Socket, errors.SendError) { 77 + use host <- result.try( 78 + complete_request.uri.host 79 + |> option.to_result(errors.EncodeRequestError(errors.NotAValidUri)), 80 + ) 81 + use scheme <- result.try( 82 + complete_request.uri.scheme 83 + |> option.to_result(errors.EncodeRequestError(errors.NotAValidUri)), 84 + ) 85 + use scheme <- result.try( 86 + http.scheme_from_string(scheme) 87 + |> result.replace_error(errors.EncodeRequestError(errors.NotAValidUri)), 88 + ) 89 + 90 + mug.new( 91 + host, 92 + complete_request.uri.port |> option.unwrap(scheme_to_port(scheme)), 93 + ) 94 + |> mug.timeout(complete_request.timeout) 95 + |> case scheme { 96 + http.Https -> mug.tls 97 + _ -> function.identity 98 + } 99 + |> mug.connect 100 + |> result.map_error(errors.FailedToConnect) 101 + } 102 + 103 + fn internal_request( 104 + socket: mug.Socket, 105 + complete_request: CompleteRequest, 106 + redirects: Int, 107 + ) -> Result(response.Response(String), errors.SendError) { 108 + let message_request = http_codec.encode_request(complete_request) 109 + use _ <- result.try( 110 + mug.send(socket, message_request) 111 + |> result.map_error(errors.SocketSendError), 112 + ) 113 + 114 + use response <- result.try(read_response(socket, complete_request)) 115 + 116 + case response.status, response.get_header(response, "location") { 117 + // TODO: support 300 multiple choice via location header 118 + 301, Ok(location) 119 + | 302, Ok(location) 120 + | 303, Ok(location) 121 + | 307, Ok(location) 122 + | 308, Ok(location) 123 + -> { 124 + let redirects = redirects + 1 125 + case 126 + redirects <= complete_request.max_redirects 127 + || complete_request.max_redirects < 0 128 + { 129 + True -> { 130 + use uri <- result.try( 131 + uri.parse(location) 132 + |> result.replace_error(errors.EncodeRequestError( 133 + errors.FailedToParseUri, 134 + )), 135 + ) 136 + use host <- result.try(option.to_result( 137 + uri.host, 138 + errors.EncodeRequestError(errors.FailedToParseUri), 139 + )) 140 + let new_request = 141 + CompleteRequest( 142 + ..complete_request, 143 + uri:, 144 + headers: complete_request.headers 145 + |> list.key_set("host", host), 146 + ) 147 + 148 + let _ = mug.shutdown(socket) 149 + use new_socket <- result.try(new_socket(new_request)) 150 + 151 + internal_request(new_socket, new_request, redirects) 152 + } 153 + False -> Ok(response) 154 + } 155 + } 156 + _, _ -> Ok(response) 157 + } 158 + } 159 + 160 + fn read_response( 161 + socket: mug.Socket, 162 + request: CompleteRequest, 163 + ) -> Result(response.Response(String), errors.SendError) { 164 + read_response_loop(<<>>, socket, request) 165 + } 166 + 167 + fn read_response_loop( 168 + acc: BitArray, 169 + socket: mug.Socket, 170 + request: CompleteRequest, 171 + ) -> Result(response.Response(String), errors.SendError) { 172 + use message_response <- result.try( 173 + mug.receive(socket, request.timeout) 174 + |> result.map_error(errors.SocketRecvError), 175 + ) 176 + let acc = bit_array.append(acc, message_response) 177 + case http_codec.decode_response(acc) { 178 + Ok(response) -> Ok(response) 179 + Error(errors.IncompleteMessage) -> read_response_loop(acc, socket, request) 180 + Error(error) -> Error(errors.DecodeResponseError(error)) 181 + } 182 + } 183 + 184 + fn scheme_to_port(scheme: http.Scheme) -> Int { 185 + case scheme { 186 + http.Http -> 80 187 + http.Https -> 443 188 + } 189 + }
+24
src/requwu/errors.gleam
··· 1 + import mug 2 + 3 + pub type EncodeRequestError { 4 + FailedToParseUri 5 + NotAValidUri 6 + } 7 + 8 + pub type DecodeResponseError { 9 + InvalidHttpVersion(BitArray) 10 + IncompleteMessage 11 + MalformedMessage 12 + MalformedHeaderTable 13 + MalformedChunkedContent 14 + IntParseFailed 15 + BitArrayConversionFailed 16 + } 17 + 18 + pub type SendError { 19 + FailedToConnect(mug.ConnectError) 20 + SocketSendError(mug.Error) 21 + SocketRecvError(mug.Error) 22 + EncodeRequestError(EncodeRequestError) 23 + DecodeResponseError(DecodeResponseError) 24 + }
+80
src/requwu/internal/ascii_util.gleam
··· 1 + import gleam/bit_array 2 + import gleam/list 3 + 4 + pub fn take_nth(bits: BitArray, n: Int) -> #(BitArray, BitArray) { 5 + case n { 6 + 0 -> #(<<>>, bits) 7 + _ -> take_nth_loop(<<>>, bits, n) 8 + } 9 + } 10 + 11 + fn take_nth_loop(acc: BitArray, rest: BitArray, n: Int) -> #(BitArray, BitArray) { 12 + case n { 13 + 0 -> #(acc, rest) 14 + _ -> 15 + case rest { 16 + <<char:unsigned-int, rest:bits>> -> 17 + take_nth_loop(<<acc:bits, char>>, rest, n - 1) 18 + // return if rest = <<>> while n > 0 19 + _ -> #(acc, rest) 20 + } 21 + } 22 + } 23 + 24 + pub fn take_till(bits: BitArray, needle: BitArray) -> #(BitArray, BitArray) { 25 + case needle { 26 + <<>> -> #(bits, <<>>) 27 + _ -> take_till_loop(<<>>, bits, needle) 28 + } 29 + } 30 + 31 + fn take_till_loop( 32 + acc: BitArray, 33 + rest: BitArray, 34 + needle: BitArray, 35 + ) -> #(BitArray, BitArray) { 36 + case rest { 37 + <<>> -> #(acc, <<>>) 38 + _ -> { 39 + // check if nth part of rest is actually the needle we need 40 + let #(match, _) = take_nth(rest, bit_array.byte_size(needle)) 41 + case match { 42 + _ if match == needle -> #(acc, rest) 43 + _ -> { 44 + // trim by 1 if it's not because it needs accuracy 45 + let #(char, rest) = take_nth(rest, 1) 46 + take_till_loop(<<acc:bits, char:bits>>, rest, needle) 47 + } 48 + } 49 + } 50 + } 51 + } 52 + 53 + pub fn contains(bits: BitArray, needles: List(BitArray)) -> Bool { 54 + case needles { 55 + [] -> True 56 + _ -> contains_loop(bits, needles) 57 + } 58 + } 59 + 60 + fn contains_loop(rest: BitArray, needles: List(BitArray)) -> Bool { 61 + case rest { 62 + <<>> -> False 63 + _ -> { 64 + let match = 65 + // check if the start of bitarray is actually one of the needles 66 + list.any(needles, fn(needle) { 67 + let #(match, _) = take_nth(rest, bit_array.byte_size(needle)) 68 + match == needle 69 + }) 70 + case match { 71 + False -> { 72 + // trim by 1 if it's not 73 + let #(_, rest) = take_nth(rest, 1) 74 + contains_loop(rest, needles) 75 + } 76 + _ -> True 77 + } 78 + } 79 + } 80 + }
+231
src/requwu/internal/http_codec.gleam
··· 1 + import given 2 + import gleam/bit_array 3 + import gleam/bytes_tree as btree 4 + import gleam/http 5 + import gleam/http/response 6 + import gleam/int 7 + import gleam/list 8 + import gleam/option 9 + import gleam/result 10 + import gleam/string 11 + import requwu/errors 12 + import requwu/internal/ascii_util 13 + import requwu/request 14 + 15 + const crlf: BitArray = <<"\r\n">> 16 + 17 + const space: BitArray = <<" ">> 18 + 19 + pub fn encode_request(request: request.CompleteRequest) -> BitArray { 20 + let query = 21 + request.uri.query 22 + |> option.map(fn(query) { "?" <> query }) 23 + |> option.unwrap("") 24 + 25 + let request_line = 26 + btree.new() 27 + |> btree.append_string(http.method_to_string(request.method)) 28 + |> btree.append(space) 29 + |> btree.append_string(normalise_path(request.uri.path) <> query) 30 + |> btree.append(space) 31 + |> btree.append_string(http_version_to_string(request.version)) 32 + 33 + let headers = 34 + list.map(request.headers, fn(header) { 35 + let #(name, value) = header 36 + name <> ": " <> value 37 + }) 38 + |> string.join("\r\n") 39 + |> string.to_option 40 + |> option.map(fn(headers) { headers <> "\r\n" }) 41 + |> option.unwrap("") 42 + 43 + btree.new() 44 + |> btree.append_tree(request_line) 45 + |> btree.append(crlf) 46 + |> btree.append_string(headers) 47 + |> btree.append(crlf) 48 + |> btree.append_string(request.body) 49 + |> btree.to_bit_array 50 + } 51 + 52 + pub fn decode_response( 53 + msg: BitArray, 54 + ) -> Result(response.Response(String), errors.DecodeResponseError) { 55 + // * ascii_util.take_nth(rest, 1) means that we throw away space 56 + // * ascii_util.take_nth(rest, 2) means that we throw away CRLF 57 + 58 + let #(http_version, rest) = ascii_util.take_till(msg, space) 59 + use <- given.that(http_version == <<"HTTP/1.1">>, else_return: fn() { 60 + Error(errors.InvalidHttpVersion(http_version)) 61 + }) 62 + 63 + let #(_, rest) = ascii_util.take_nth(rest, 1) 64 + let #(status_code, rest) = ascii_util.take_till(rest, space) 65 + use status_code <- result.try( 66 + bit_array.to_string(status_code) 67 + |> result.replace_error(errors.BitArrayConversionFailed), 68 + ) 69 + use status <- result.try( 70 + int.base_parse(status_code, 10) 71 + |> result.replace_error(errors.IntParseFailed), 72 + ) 73 + 74 + let #(_, rest) = ascii_util.take_nth(rest, 1) 75 + let #(_reason, rest) = ascii_util.take_till(rest, crlf) 76 + 77 + let #(_, rest) = ascii_util.take_nth(rest, 2) 78 + use #(headers, rest) <- result.try(parse_header_loop([], rest)) 79 + 80 + let #(_, body) = ascii_util.take_nth(rest, 2) 81 + case 82 + list.key_find(headers, "content-length"), 83 + list.key_find(headers, "transfer-encoding") 84 + |> result.unwrap("") 85 + |> string.contains("chunked") 86 + { 87 + Ok(length), False -> { 88 + use length <- result.try( 89 + int.base_parse(length, 10) 90 + |> result.replace_error(errors.IntParseFailed), 91 + ) 92 + case bit_array.byte_size(body) < length { 93 + True -> Error(errors.IncompleteMessage) 94 + False -> { 95 + use body <- result.try( 96 + bit_array.to_string(body) 97 + |> result.replace_error(errors.BitArrayConversionFailed), 98 + ) 99 + Ok(response.Response(status:, headers:, body:)) 100 + } 101 + } 102 + } 103 + Error(Nil), True -> { 104 + // check if at the end of body is CRLF 105 + case bit_array.slice(body, bit_array.byte_size(body), -2) { 106 + Ok(<<"\r\n">>) -> { 107 + use body <- result.try(unwrap_chunked(body)) 108 + use body <- result.try( 109 + bit_array.to_string(body) 110 + |> result.replace_error(errors.BitArrayConversionFailed), 111 + ) 112 + Ok(response.Response(status:, headers:, body:)) 113 + } 114 + _ -> Error(errors.IncompleteMessage) 115 + } 116 + } 117 + _, _ -> Error(errors.MalformedMessage) 118 + } 119 + } 120 + 121 + fn http_version_to_string(version: request.HttpVersion) -> String { 122 + "HTTP/" 123 + <> case version { 124 + request.Http11 -> "1.1" 125 + } 126 + } 127 + 128 + fn normalise_path(path: String) { 129 + case path { 130 + "" -> "/" 131 + _ -> path 132 + } 133 + } 134 + 135 + fn parse_header_loop( 136 + acc: List(#(String, String)), 137 + rest: BitArray, 138 + ) -> Result(#(List(#(String, String)), BitArray), errors.DecodeResponseError) { 139 + let #(match, _) = ascii_util.take_nth(rest, 2) 140 + case match { 141 + // if we found CRLF, that means we found an end 142 + <<"\r\n">> -> Ok(#(acc, rest)) 143 + _ -> { 144 + let #(name, rest) = ascii_util.take_till(rest, <<": ">>) 145 + use <- given.not(name == <<>>, else_return: fn() { 146 + Error(errors.MalformedHeaderTable) 147 + }) 148 + 149 + // throw ": " away 150 + let #(_, rest) = ascii_util.take_nth(rest, 2) 151 + let #(value, rest) = ascii_util.take_till(rest, crlf) 152 + 153 + use name <- result.try( 154 + bit_array.to_string(name) 155 + |> result.replace_error(errors.BitArrayConversionFailed), 156 + ) 157 + use value <- result.try( 158 + bit_array.to_string(value) 159 + |> result.replace_error(errors.BitArrayConversionFailed), 160 + ) 161 + let name = string.lowercase(name) 162 + let value = string.lowercase(value) |> string.trim_end 163 + 164 + // throw CRLF away so the first case doesn't get messed up 165 + let #(_, rest) = ascii_util.take_nth(rest, 2) 166 + parse_header_loop([#(name, value), ..acc], rest) 167 + } 168 + } 169 + } 170 + 171 + pub fn unwrap_chunked( 172 + chunks: BitArray, 173 + ) -> Result(BitArray, errors.DecodeResponseError) { 174 + unwrap_chunk_loop(<<>>, chunks) 175 + } 176 + 177 + fn unwrap_chunk_loop( 178 + acc: BitArray, 179 + chunks: BitArray, 180 + ) -> Result(BitArray, errors.DecodeResponseError) { 181 + use #(chunk, rest) <- result.try(unwrap_single_chunk(chunks)) 182 + case chunk { 183 + <<>> -> Ok(acc) 184 + _ -> unwrap_chunk_loop(<<acc:bits, chunk:bits>>, rest) 185 + } 186 + } 187 + 188 + fn unwrap_single_chunk( 189 + chunks: BitArray, 190 + ) -> Result(#(BitArray, BitArray), errors.DecodeResponseError) { 191 + // assume we're after last chunk, take till CRLF 192 + let #(chunk_size, rest) = ascii_util.take_till(chunks, crlf) 193 + // chunk_ext might be exist, let's just ignore that 194 + let #(chunk_size, _chunk_ext) = ascii_util.take_till(chunk_size, <<";">>) 195 + 196 + // if both chunk_size and rest empty, it means that we're parsing incomplete message 197 + 198 + use chunk_size <- result.try( 199 + bit_array.to_string(chunk_size) 200 + |> result.replace_error(case chunk_size, rest { 201 + <<>>, <<>> -> errors.IncompleteMessage 202 + _, _ -> errors.MalformedChunkedContent 203 + }), 204 + ) 205 + use chunk_size <- result.try( 206 + int.base_parse(chunk_size, 16) 207 + |> result.replace_error(case chunk_size, rest { 208 + "", <<>> -> errors.IncompleteMessage 209 + _, _ -> errors.MalformedChunkedContent 210 + }), 211 + ) 212 + 213 + let #(_, rest) = ascii_util.take_nth(rest, 2) 214 + 215 + case chunk_size { 216 + // the "actual" chunk_data of last-chunk is trailers, let's discard that 217 + 0 -> Ok(#(<<>>, rest)) 218 + _ -> { 219 + let #(chunk_data, rest) = ascii_util.take_till(rest, crlf) 220 + 221 + use <- given.that( 222 + // we check if the chunk_data's size is the same as the chunk_size 223 + bit_array.byte_size(chunk_data) == chunk_size, 224 + else_return: fn() { Error(errors.MalformedChunkedContent) }, 225 + ) 226 + 227 + let #(_, rest) = ascii_util.take_nth(rest, 2) 228 + Ok(#(chunk_data, rest)) 229 + } 230 + } 231 + }
+215
src/requwu/request.gleam
··· 1 + import gleam/bit_array 2 + import gleam/function 3 + import gleam/http 4 + import gleam/http/request 5 + import gleam/int 6 + import gleam/json 7 + import gleam/list 8 + import gleam/option.{type Option, None, Some} 9 + import gleam/result 10 + import gleam/string 11 + import gleam/string_tree 12 + import gleam/uri 13 + import requwu/errors 14 + 15 + pub type RequestBuilder { 16 + RequestBuilder( 17 + timeout: Int, 18 + method: http.Method, 19 + uri: String, 20 + query: Option(String), 21 + version: HttpVersion, 22 + headers: List(#(String, String)), 23 + auth: Option(String), 24 + user_agent: Option(String), 25 + max_redirects: Int, 26 + body: String, 27 + ) 28 + } 29 + 30 + pub type HttpVersion { 31 + Http11 32 + } 33 + 34 + pub fn to(method: http.Method, to uri: String) -> RequestBuilder { 35 + let #(uri, query) = 36 + string.split_once(uri, on: "?") |> result.unwrap(#(uri, "")) 37 + 38 + RequestBuilder( 39 + timeout: 1000, 40 + method:, 41 + uri:, 42 + query: string.to_option(query), 43 + version: Http11, 44 + headers: [], 45 + auth: None, 46 + user_agent: None, 47 + max_redirects: -1, 48 + body: "", 49 + ) 50 + } 51 + 52 + pub fn get(uri: String) -> RequestBuilder { 53 + to(http.Get, uri) 54 + } 55 + 56 + pub fn head(uri: String) -> RequestBuilder { 57 + to(http.Head, uri) 58 + } 59 + 60 + pub fn post(uri: String) -> RequestBuilder { 61 + to(http.Post, uri) 62 + } 63 + 64 + pub fn put(uri: String) -> RequestBuilder { 65 + to(http.Put, uri) 66 + } 67 + 68 + pub fn delete(uri: String) -> RequestBuilder { 69 + to(http.Delete, uri) 70 + } 71 + 72 + pub fn connect(uri: String) -> RequestBuilder { 73 + to(http.Connect, uri) 74 + } 75 + 76 + pub fn options(uri: String) -> RequestBuilder { 77 + to(http.Options, uri) 78 + } 79 + 80 + pub fn trace(uri: String) -> RequestBuilder { 81 + to(http.Trace, uri) 82 + } 83 + 84 + pub fn header( 85 + builder: RequestBuilder, 86 + key: String, 87 + value: String, 88 + ) -> RequestBuilder { 89 + RequestBuilder(..builder, headers: [#(key, value), ..builder.headers]) 90 + } 91 + 92 + pub fn headers( 93 + builder: RequestBuilder, 94 + headers: List(#(String, String)), 95 + ) -> RequestBuilder { 96 + RequestBuilder(..builder, headers: list.append(builder.headers, headers)) 97 + } 98 + 99 + pub fn body(builder: RequestBuilder, body: String) -> RequestBuilder { 100 + RequestBuilder(..builder, body:) 101 + } 102 + 103 + pub fn json(builder: RequestBuilder, json: json.Json) -> RequestBuilder { 104 + RequestBuilder( 105 + ..builder, 106 + body: json.to_string_tree(json) |> string_tree.to_string, 107 + ) 108 + |> header("content-type", "application/json") 109 + } 110 + 111 + pub fn basic_auth( 112 + builder: RequestBuilder, 113 + user: String, 114 + password: Option(String), 115 + ) -> RequestBuilder { 116 + let basic_auth = 117 + bit_array.from_string(user <> ":" <> password |> option.unwrap("")) 118 + |> bit_array.base64_encode(True) 119 + 120 + RequestBuilder(..builder, auth: Some("Basic " <> basic_auth)) 121 + } 122 + 123 + pub fn bearer_auth(builder: RequestBuilder, token: String) -> RequestBuilder { 124 + RequestBuilder(..builder, auth: Some("Bearer " <> token)) 125 + } 126 + 127 + pub fn auth(builder: RequestBuilder, auth: String) -> RequestBuilder { 128 + RequestBuilder(..builder, auth: Some(auth)) 129 + } 130 + 131 + pub fn user_agent(builder: RequestBuilder, user_agent: String) -> RequestBuilder { 132 + RequestBuilder(..builder, user_agent: Some(user_agent)) 133 + } 134 + 135 + pub fn max_redirects(builder: RequestBuilder, redirects: Int) { 136 + RequestBuilder(..builder, max_redirects: redirects) 137 + } 138 + 139 + pub fn timeout( 140 + builder: RequestBuilder, 141 + miliseconds timeout: Int, 142 + ) -> RequestBuilder { 143 + RequestBuilder(..builder, timeout:) 144 + } 145 + 146 + pub fn query(builder: RequestBuilder, query: String) -> RequestBuilder { 147 + RequestBuilder(..builder, query: Some(query)) 148 + } 149 + 150 + pub fn from_request(req: request.Request(String)) -> RequestBuilder { 151 + let uri = request.to_uri(req) |> uri.to_string 152 + 153 + to(req.method, uri) 154 + |> headers(req.headers) 155 + |> case list.key_find(req.headers, "authorization") { 156 + Error(Nil) -> function.identity 157 + Ok(auth_str) -> auth(_, auth_str) 158 + } 159 + |> body(req.body) 160 + } 161 + 162 + @internal 163 + pub type CompleteRequest { 164 + CompleteRequest( 165 + timeout: Int, 166 + method: http.Method, 167 + uri: uri.Uri, 168 + version: HttpVersion, 169 + headers: List(#(String, String)), 170 + max_redirects: Int, 171 + body: String, 172 + ) 173 + } 174 + 175 + @internal 176 + pub fn complete( 177 + builder: RequestBuilder, 178 + ) -> Result(CompleteRequest, errors.EncodeRequestError) { 179 + let query = case builder.query { 180 + None -> "" 181 + Some(query) -> "?" <> query 182 + } 183 + let uri = builder.uri <> query 184 + use uri <- result.try( 185 + uri.parse(uri) |> result.replace_error(errors.FailedToParseUri), 186 + ) 187 + 188 + use host <- result.try(option.to_result(uri.host, errors.FailedToParseUri)) 189 + 190 + let headers = { 191 + case builder.auth { 192 + None -> [] 193 + Some(auth) -> [#("authorization", auth)] 194 + } 195 + |> list.append(builder.headers) 196 + |> list.prepend(#("host", host)) 197 + |> list.prepend(#( 198 + "content-length", 199 + bit_array.from_string(builder.body) 200 + |> bit_array.byte_size 201 + |> int.to_string, 202 + )) 203 + |> list.prepend(#("te", "chunked")) 204 + } 205 + 206 + Ok(CompleteRequest( 207 + timeout: builder.timeout, 208 + method: builder.method, 209 + uri:, 210 + version: builder.version, 211 + headers:, 212 + max_redirects: builder.max_redirects, 213 + body: builder.body, 214 + )) 215 + }