From 5c9cb23ec7d0893f4d3be4b2cc5afe1b43bb255d Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 10 Nov 2024 16:38:25 +0000 Subject: [PATCH] v4.0.0 --- CHANGELOG.md | 5 ++++ gleam.toml | 2 +- src/gleam/httpc.gleam | 56 ++++++++++++++++++++++++------------- src/gleam_httpc_ffi.erl | 22 ++++++++++++++- test/gleam_httpc_test.gleam | 10 +++++-- 5 files changed, 72 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c736c4..dfe2ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v4.0.0 - 2024-11-10 + +- The `HttpError` type has been introduced and is now used by the send + functions. + ## v3.0.0 - 2024-10-02 - A default user agent is set if a request doesn't have now. diff --git a/gleam.toml b/gleam.toml index c616895..b45857d 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "gleam_httpc" -version = "3.0.0" +version = "4.0.0" gleam = ">= 1.0.0" licences = ["Apache-2.0"] diff --git a/src/gleam/httpc.gleam b/src/gleam/httpc.gleam index aa9381f..e50a40e 100644 --- a/src/gleam/httpc.gleam +++ b/src/gleam/httpc.gleam @@ -8,9 +8,22 @@ import gleam/list import gleam/result import gleam/uri +pub type HttpError { + InvalidUtf8Response + FailedToConnect(ip4: ConnectError, ip6: ConnectError) +} + +pub type ConnectError { + Posix(code: String) + TlsAlert(code: String, detail: String) +} + @external(erlang, "gleam_httpc_ffi", "default_user_agent") fn default_user_agent() -> #(Charlist, Charlist) +@external(erlang, "gleam_httpc_ffi", "normalise_error") +fn normalise_error(error: Dynamic) -> HttpError + type ErlHttpOption { Ssl(List(ErlSslOption)) Autoredirect(Bool) @@ -73,7 +86,9 @@ fn string_header(header: #(Charlist, Charlist)) -> #(String, String) { /// /// If you wish to use some other configuration use `dispatch_bits` instead. /// -pub fn send_bits(req: Request(BitArray)) -> Result(Response(BitArray), Dynamic) { +pub fn send_bits( + req: Request(BitArray), +) -> Result(Response(BitArray), HttpError) { configure() |> dispatch_bits(req) } @@ -84,7 +99,7 @@ pub fn send_bits(req: Request(BitArray)) -> Result(Response(BitArray), Dynamic) pub fn dispatch_bits( config: Configuration, req: Request(BitArray), -) -> Result(Response(BitArray), Dynamic) { +) -> Result(Response(BitArray), HttpError) { let erl_url = req |> request.to_uri @@ -98,21 +113,24 @@ pub fn dispatch_bits( } let erl_options = [BodyFormat(Binary), SocketOpts([Ipfamily(Inet6fb4)])] - use response <- result.then(case req.method { - http.Options | http.Head | http.Get -> { - let erl_req = #(erl_url, erl_headers) - erl_request_no_body(req.method, erl_req, erl_http_options, erl_options) + use response <- result.then( + case req.method { + http.Options | http.Head | http.Get -> { + let erl_req = #(erl_url, erl_headers) + erl_request_no_body(req.method, erl_req, erl_http_options, erl_options) + } + _ -> { + let erl_content_type = + req + |> request.get_header("content-type") + |> result.unwrap("application/octet-stream") + |> charlist.from_string + let erl_req = #(erl_url, erl_headers, erl_content_type, req.body) + erl_request(req.method, erl_req, erl_http_options, erl_options) + } } - _ -> { - let erl_content_type = - req - |> request.get_header("content-type") - |> result.unwrap("application/octet-stream") - |> charlist.from_string - let erl_req = #(erl_url, erl_headers, erl_content_type, req.body) - erl_request(req.method, erl_req, erl_http_options, erl_options) - } - }) + |> result.map_error(normalise_error), + ) let #(#(_version, status, _status), headers, resp_body) = response Ok(Response(status, list.map(headers, string_header), resp_body)) @@ -161,13 +179,13 @@ pub fn verify_tls(_config: Configuration, which: Bool) -> Configuration { pub fn dispatch( config: Configuration, request: Request(String), -) -> Result(Response(String), Dynamic) { +) -> Result(Response(String), HttpError) { let request = request.map(request, bit_array.from_string) use resp <- result.try(dispatch_bits(config, request)) case bit_array.to_string(resp.body) { Ok(body) -> Ok(response.set_body(resp, body)) - Error(_) -> Error(dynamic.from("Response body was not valid UTF-8")) + Error(_) -> Error(InvalidUtf8Response) } } @@ -176,7 +194,7 @@ pub fn dispatch( /// /// If you wish to use some other configuration use `dispatch` instead. /// -pub fn send(req: Request(String)) -> Result(Response(String), Dynamic) { +pub fn send(req: Request(String)) -> Result(Response(String), HttpError) { configure() |> dispatch(req) } diff --git a/src/gleam_httpc_ffi.erl b/src/gleam_httpc_ffi.erl index 2fd6244..9bf21cf 100644 --- a/src/gleam_httpc_ffi.erl +++ b/src/gleam_httpc_ffi.erl @@ -1,5 +1,25 @@ -module(gleam_httpc_ffi). --export([default_user_agent/0]). +-export([default_user_agent/0, normalise_error/1]). + +normalise_error(Error = {failed_connect, Opts}) -> + Ipv6 = case lists:keyfind(inet6, 1, Opts) of + {inet6, _, V1} -> V1; + _ -> erlang:error({unexpected_httpc_error, Error}) + end, + Ipv4 = case lists:keyfind(inet, 1, Opts) of + {inet, _, V2} -> V2; + _ -> erlang:error({unexpected_httpc_error, Error}) + end, + {failed_to_connect, normalise_ip_error(Ipv4), normalise_ip_error(Ipv6)}; +normalise_error(Error) -> + erlang:error({unexpected_httpc_error, Error}). + +normalise_ip_error(Code) when is_atom(Code) -> + {posix, erlang:atom_to_binary(Code)}; +normalise_ip_error({tls_alert, {A, B}}) -> + {tls_alert, erlang:atom_to_binary(A), unicode:characters_to_binary(B)}; +normalise_ip_error(Error) -> + erlang:error({unexpected_httpc_ip_error, Error}). default_user_agent() -> Version = diff --git a/test/gleam_httpc_test.gleam b/test/gleam_httpc_test.gleam index 5d8bd48..cfca369 100644 --- a/test/gleam_httpc_test.gleam +++ b/test/gleam_httpc_test.gleam @@ -72,10 +72,16 @@ pub fn invalid_tls_test() { let assert Ok(req) = request.to("https://expired.badssl.com") // This will fail because of invalid TLS - let assert Error(_e) = httpc.send(req) + let assert Error(httpc.FailedToConnect( + ip4: httpc.TlsAlert("certificate_expired", _), + ip6: _, + )) = httpc.send(req) // This will fail because of invalid TLS - let assert Error(_e) = + let assert Error(httpc.FailedToConnect( + ip4: httpc.TlsAlert("certificate_expired", _), + ip6: _, + )) = httpc.configure() |> httpc.verify_tls(True) |> httpc.dispatch(req)