Skip to content

Commit

Permalink
v4.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
lpil committed Nov 10, 2024
1 parent 16d90a1 commit 5c9cb23
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 23 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "gleam_httpc"
version = "3.0.0"
version = "4.0.0"
gleam = ">= 1.0.0"

licences = ["Apache-2.0"]
Expand Down
56 changes: 37 additions & 19 deletions src/gleam/httpc.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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)
}
Expand Down
22 changes: 21 additions & 1 deletion src/gleam_httpc_ffi.erl
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down
10 changes: 8 additions & 2 deletions test/gleam_httpc_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 5c9cb23

Please sign in to comment.