Skip to content

Commit

Permalink
HTTP2 support (#48)
Browse files Browse the repository at this point in the history
* add protocols flag

* change codebase to adapt to default http2 from curl
  • Loading branch information
kevinschweikert authored Mar 9, 2025
1 parent f932cdf commit dee56a2
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 61 deletions.
46 changes: 39 additions & 7 deletions lib/curl_req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,29 @@ defmodule CurlReq do
## Examples
iex> CurlReq.from_curl("curl https://www.example.com")
%Req.Request{method: :get, url: URI.parse("https://www.example.com")}
%Req.Request{
method: :get,
url: URI.parse("https://www.example.com"),
registered_options: MapSet.new([:connect_options]),
options: %{connect_options: [protocols: [:http1, :http2]]}
}
iex> CurlReq.from_curl("curl -I https://example.com")
%Req.Request{method: :head, url: URI.parse("https://example.com")}
%Req.Request{
method: :head,
url: URI.parse("https://example.com"),
registered_options: MapSet.new([:connect_options]),
options: %{connect_options: [protocols: [:http1, :http2]]}
}
iex> CurlReq.from_curl("curl -b cookie_key=cookie_val https://example.com")
%Req.Request{method: :get, headers: %{"cookie" => ["cookie_key=cookie_val"]}, url: URI.parse("https://example.com")}
%Req.Request{
method: :get,
headers: %{"cookie" => ["cookie_key=cookie_val"]},
url: URI.parse("https://example.com"),
registered_options: MapSet.new([:connect_options]),
options: %{connect_options: [protocols: [:http1, :http2]]}
}
"""
@doc since: "0.98.4"

Expand All @@ -165,21 +181,37 @@ defmodule CurlReq do
Remember to
```elixir
import CurlReq
require CurlReq
```
to use the custom sigil.
## Examples
iex> ~CURL(curl "https://www.example.com")
%Req.Request{method: :get, url: URI.parse("https://www.example.com")}
%Req.Request{
method: :get,
url: URI.parse("https://www.example.com"),
registered_options: MapSet.new([:connect_options]),
options: %{connect_options: [protocols: [:http1, :http2]]}
}
iex> ~CURL(curl -I "https://example.com")
%Req.Request{method: :head, url: URI.parse("https://example.com")}
%Req.Request{
method: :head,
url: URI.parse("https://example.com"),
registered_options: MapSet.new([:connect_options]),
options: %{connect_options: [protocols: [:http1, :http2]]}
}
iex> ~CURL(curl -b "cookie_key=cookie_val" "https://example.com")
%Req.Request{method: :get, headers: %{"cookie" => ["cookie_key=cookie_val"]}, url: URI.parse("https://example.com")}
%Req.Request{
method: :get,
headers: %{"cookie" => ["cookie_key=cookie_val"]},
url: URI.parse("https://example.com"),
registered_options: MapSet.new([:connect_options]),
options: %{connect_options: [protocols: [:http1, :http2]]}
}
"""
defmacro sigil_CURL(curl_command, modifiers)

Expand Down
42 changes: 40 additions & 2 deletions lib/curl_req/curl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ defmodule CurlReq.Curl do
show_error: :boolean,
output: :string,
remote_name: :boolean,
verbose: :boolean
verbose: :boolean,
"http1.0": :boolean,
"http1.1": :boolean,
http2: :boolean,
http2_prior_knowledge: :boolean
]

@aliases [
Expand All @@ -50,7 +54,8 @@ defmodule CurlReq.Curl do
S: :show_error,
o: :output,
O: :remote_name,
v: :verbose
v: :verbose,
"0": :"http1.0"
]

@doc """
Expand Down Expand Up @@ -150,6 +155,7 @@ defmodule CurlReq.Curl do
|> add_insecure(options)
|> add_user_agent(options)
|> add_method(options)
|> add_protocols(options)
|> configure_redirects(options)
end

Expand Down Expand Up @@ -261,6 +267,24 @@ defmodule CurlReq.Curl do
CurlReq.Request.put_user_agent(request, options[:user_agent])
end

defp add_protocols(request, options) do
http1_0 = Keyword.get(options, :"http1.0", false)
http1_1 = Keyword.get(options, :"http1.1", false)
http2 = Keyword.get(options, :http2, false)
http2_prior_knowledge = Keyword.get(options, :http2_prior_knowledge, false)

protos = []
protos = if http1_0, do: [:http1_0 | protos], else: protos
protos = if http1_1, do: [:http1_1 | protos], else: protos
protos = if http2, do: [:http2, :http1_1 | protos], else: protos
protos = if http2_prior_knowledge, do: [:http2], else: protos

# cURL default
protos = if protos == [], do: [:http1_1, :http2], else: protos

CurlReq.Request.put_protocols(request, protos)
end

defp configure_redirects(request, options) do
CurlReq.Request.put_redirect(request, options[:location])
end
Expand Down Expand Up @@ -383,11 +407,20 @@ defmodule CurlReq.Curl do

insecure = if request.insecure, do: [insecure_flag(flag_style)], else: []

protocols =
if :http2 in request.protocols do
# don't add flags for default behavior
[]
else
for proto <- request.protocols, do: protocol_flag(flag_style, proto)
end

url = [" ", request.url |> to_string() |> escape()]

IO.iodata_to_binary([
"curl",
compressed,
protocols,
insecure,
auth,
headers,
Expand Down Expand Up @@ -460,6 +493,11 @@ defmodule CurlReq.Curl do
defp insecure_flag(:short), do: " -k"
defp insecure_flag(:long), do: " --insecure"

defp protocol_flag(:short, :http1_0), do: " -0"
defp protocol_flag(:long, :http1_0), do: " --http1.0"
defp protocol_flag(_, :http1_1), do: " --http1.1"
defp protocol_flag(_, :http2), do: " --http2"

defp user_agent_flag(:short, value), do: [" -A ", escape(value)]
defp user_agent_flag(:long, value), do: [" --user-agent ", escape(value)]
end
35 changes: 35 additions & 0 deletions lib/curl_req/req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ defmodule CurlReq.Req do
nil
end

request =
case Keyword.get(connect_options, :protocols) do
nil ->
request

protocols ->
mapped_protos =
for proto <- protocols do
case proto do
:http1 -> [:http1_0, :http1_1]
:http2 -> [:http2]
end
end

CurlReq.Request.put_protocols(request, List.flatten(mapped_protos))
end

request =
with transport_opts <- Keyword.get(connect_options, :transport_opts, []),
:verify_none <- Keyword.get(transport_opts, :verify) do
Expand Down Expand Up @@ -179,6 +196,23 @@ defmodule CurlReq.Req do
[]
end

protocols =
request.protocols
|> Enum.map(fn
:http1_0 -> :http1
:http1_1 -> :http1
:http2 -> :http2
end)
|> Enum.uniq()

protocols =
if protocols == [:http1] do
# do not set default values
[]
else
[protocols: protocols]
end

transport_opts =
if request.insecure do
[transport_opts: [verify: :verify_none]]
Expand All @@ -191,6 +225,7 @@ defmodule CurlReq.Req do
|> Keyword.merge(proxy)
|> Keyword.merge(proxy_auth)
|> Keyword.merge(transport_opts)
|> Keyword.merge(protocols)

req =
if connect_options != [] do
Expand Down
18 changes: 16 additions & 2 deletions lib/curl_req/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defmodule CurlReq.Request do
@doc "decode from #{__MODULE__} to the destination type"
@callback decode(__MODULE__.t(), opts :: Keyword.t()) :: term()

@protocols [:http1_0, :http1_1, :http2]

@type t() :: %__MODULE__{
user_agent: user_agent(),
headers: header(),
Expand All @@ -24,7 +26,8 @@ defmodule CurlReq.Request do
encoding: encoding(),
body: term(),
raw_body: String.t(),
insecure: boolean()
insecure: boolean(),
protocols: protocols()
}

@type user_agent() :: :curl | :req | String.t()
Expand All @@ -35,6 +38,7 @@ defmodule CurlReq.Request do
@type encoding() :: :raw | :form | :json
@type method() :: :get | :head | :put | :post | :delete | :patch
@type compression() :: :none | :gzip | :br | :zstd
@type protocols() :: :http1_0 | :http1_1 | :http2

@derive {Inspect, except: [:auth]}
defstruct user_agent: :curl,
Expand All @@ -51,7 +55,8 @@ defmodule CurlReq.Request do
encoding: :raw,
body: nil,
raw_body: nil,
insecure: false
insecure: false,
protocols: [:http1_1, :http2]

@doc """
Puts the header into the CurlReq.Request struct. Special headers like encoding, authorization or user-agent are stored in their respective field in the #{__MODULE__} struct instead of a general header.
Expand Down Expand Up @@ -461,4 +466,13 @@ defmodule CurlReq.Request do
when user_agent in [:curl, :req] or is_binary(user_agent) do
%{request | user_agent: user_agent}
end

@spec put_protocols(t(), [protocols()]) :: t()
def put_protocols(%__MODULE__{} = request, protocols) when is_list(protocols) do
if not Enum.all?(protocols, fn proto -> proto in @protocols end) do
raise "Protocol must be one of #{inspect(@protocols)}, got: #{inspect(protocols)}}"
end

%{request | protocols: protocols |> Enum.uniq() |> Enum.sort()}
end
end
13 changes: 13 additions & 0 deletions test/curl_req/request_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,17 @@ defmodule CurlReq.RequestTest do
assert %{"foo" => "bar"} = request.body
end
end

describe "put_protocols/2" do
for proto <- [:http1_0, :http1_1, :http2] do
@tag proto: proto
test "set #{inspect(proto)}", %{proto: proto} do
request =
%Request{}
|> put_protocols([proto])

assert request.protocols == [proto]
end
end
end
end
Loading

0 comments on commit dee56a2

Please sign in to comment.