Repository: derekkraan/curl_req (elixir)

Issue:
Suppport HTTP2
>  -0, --http1.0       Use HTTP 1.0
>     --http1.1       Use HTTP 1.1
>     --http2         Use HTTP 2
>     --http2-prior-knowledge Use HTTP 2 without HTTP/1.1 Upgrade

These are the flags and `req` defaults to only using HTTP/1.1. If a user want's to override this behavior we should support this flag.
Also, should we default to allow HTTP/2 for `req` when parsing from a cURL command?
And the other way round, should be explicitly set the `--http1.1` flag when translating from `Req.Request` to cURL when the defaults are set?

Pull request description:
HTTP2 support
Closes #46 

All tests where we convert from cURL to Req have changed to set the HTTP/2 protocol explicitly for Req

Patch:
diff --git a/lib/curl_req.ex b/lib/curl_req.ex
index 21c71e7..4099c5c 100644
--- a/lib/curl_req.ex
+++ b/lib/curl_req.ex
@@ -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"
 
@@ -165,7 +181,7 @@ defmodule CurlReq do
   Remember to
 
   ```elixir
-  import CurlReq
+  require CurlReq
   ```
 
   to use the custom sigil.
@@ -173,13 +189,29 @@ defmodule CurlReq do
   ## 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)
 
diff --git a/lib/curl_req/curl.ex b/lib/curl_req/curl.ex
index f189e2d..0f7345f 100644
--- a/lib/curl_req/curl.ex
+++ b/lib/curl_req/curl.ex
@@ -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 [
@@ -50,7 +54,8 @@ defmodule CurlReq.Curl do
     S: :show_error,
     o: :output,
     O: :remote_name,
-    v: :verbose
+    v: :verbose,
+    "0": :"http1.0"
   ]
 
   @doc """
@@ -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
 
@@ -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
@@ -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,
@@ -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
diff --git a/lib/curl_req/req.ex b/lib/curl_req/req.ex
index 700d3dc..3988438 100644
--- a/lib/curl_req/req.ex
+++ b/lib/curl_req/req.ex
@@ -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
@@ -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]]
@@ -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
diff --git a/lib/curl_req/request.ex b/lib/curl_req/request.ex
index 2d3c691..2e4dd4f 100644
--- a/lib/curl_req/request.ex
+++ b/lib/curl_req/request.ex
@@ -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(),
@@ -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()
@@ -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,
@@ -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.
@@ -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


Test patch:
diff --git a/test/curl_req/request_test.exs b/test/curl_req/request_test.exs
index 5df3bb9..ec7a7a3 100644
--- a/test/curl_req/request_test.exs
+++ b/test/curl_req/request_test.exs
@@ -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
diff --git a/test/curl_req_test.exs b/test/curl_req_test.exs
index c453d8c..31bb1a3 100644
--- a/test/curl_req_test.exs
+++ b/test/curl_req_test.exs
@@ -233,6 +233,29 @@ defmodule CurlReqTest do
                )
                |> CurlReq.to_curl(flags: :long)
     end
+
+    test "protocols" do
+      assert ~s(curl --compressed -0 --http1.1 -X GET http://example.com) ==
+               Req.new(
+                 url: "http://example.com",
+                 connect_options: [protocols: [:http1]]
+               )
+               |> CurlReq.to_curl()
+
+      assert ~s(curl --compressed -X GET http://example.com) ==
+               Req.new(
+                 url: "http://example.com",
+                 connect_options: [protocols: [:http2]]
+               )
+               |> CurlReq.to_curl()
+
+      assert ~s(curl --compressed -X GET http://example.com) ==
+               Req.new(
+                 url: "http://example.com",
+                 connect_options: [protocols: [:http1, :http2]]
+               )
+               |> CurlReq.to_curl()
+    end
   end
 
   describe "from_curl" do
@@ -240,7 +263,11 @@ defmodule CurlReqTest do
       assert ~CURL(curl example.com/fact) ==
                %Req.Request{
                  method: :get,
-                 url: URI.parse("http://example.com/fact")
+                 url: URI.parse("http://example.com/fact"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
 
@@ -256,7 +283,11 @@ defmodule CurlReqTest do
       assert ~CURL(curl -H "user-agent: req/0.4.14" -X GET https://example.com/fact) ==
                %Req.Request{
                  method: :get,
-                 url: URI.parse("https://example.com/fact")
+                 url: URI.parse("https://example.com/fact"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
 
@@ -264,7 +295,11 @@ defmodule CurlReqTest do
       assert ~CURL(curl -X POST https://example.com) ==
                %Req.Request{
                  method: :post,
-                 url: URI.parse("https://example.com")
+                 url: URI.parse("https://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
 
@@ -272,15 +307,19 @@ defmodule CurlReqTest do
       assert ~CURL(curl -I https://example.com) ==
                %Req.Request{
                  method: :head,
-                 url: URI.parse("https://example.com")
+                 url: URI.parse("https://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
 
     test "raw body" do
       assert ~CURL(curl -d foo https://example.com) ==
                %Req.Request{
-                 options: %{form: %{"foo" => ""}},
-                 registered_options: MapSet.new([:form]),
+                 options: %{form: %{"foo" => ""}, connect_options: [protocols: [:http1, :http2]]},
+                 registered_options: MapSet.new([:form, :connect_options]),
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1],
                  url: URI.parse("https://example.com"),
@@ -291,8 +330,11 @@ defmodule CurlReqTest do
     test "form body" do
       assert ~CURL(curl -d foo=bar https://example.com) ==
                %Req.Request{
-                 options: %{form: %{"foo" => "bar"}},
-                 registered_options: MapSet.new([:form]),
+                 options: %{
+                   form: %{"foo" => "bar"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
+                 registered_options: MapSet.new([:form, :connect_options]),
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1],
                  url: URI.parse("https://example.com"),
@@ -301,8 +343,11 @@ defmodule CurlReqTest do
 
       assert ~CURL(curl -d foo=bar&baz=qux https://example.com) ==
                %Req.Request{
-                 options: %{form: %{"foo" => "bar", "baz" => "qux"}},
-                 registered_options: MapSet.new([:form]),
+                 options: %{
+                   form: %{"foo" => "bar", "baz" => "qux"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
+                 registered_options: MapSet.new([:form, :connect_options]),
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1],
                  url: URI.parse("https://example.com"),
@@ -313,8 +358,11 @@ defmodule CurlReqTest do
     test "form body with multiple data flags" do
       assert ~CURL(curl http://example.com -d name=foo -d mail=bar) ==
                %Req.Request{
-                 options: %{form: %{"name" => "foo", "mail" => "bar"}},
-                 registered_options: MapSet.new([:form]),
+                 options: %{
+                   form: %{"name" => "foo", "mail" => "bar"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
+                 registered_options: MapSet.new([:form, :connect_options]),
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1],
                  url: URI.parse("http://example.com"),
@@ -325,8 +373,11 @@ defmodule CurlReqTest do
     test "json body" do
       assert ~CURL(curl -H "content-type: application/json" -d "{\"foo\": \"bar\"}" https://example.com) ==
                %Req.Request{
-                 options: %{json: %{"foo" => "bar"}},
-                 registered_options: MapSet.new([:json]),
+                 options: %{
+                   json: %{"foo" => "bar"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
+                 registered_options: MapSet.new([:json, :connect_options]),
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1],
                  url: URI.parse("https://example.com"),
@@ -337,8 +388,11 @@ defmodule CurlReqTest do
     test "content-type with parameter" do
       assert ~CURL(curl -H "content-type: application/json; charset=utf-8" -d '{"foo": "bar"}' https://example.com) ==
                %Req.Request{
-                 options: %{json: %{"foo" => "bar"}},
-                 registered_options: MapSet.new([:json]),
+                 options: %{
+                   json: %{"foo" => "bar"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
+                 registered_options: MapSet.new([:json, :connect_options]),
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1],
                  url: URI.parse("https://example.com"),
@@ -350,7 +404,11 @@ defmodule CurlReqTest do
       assert ~CURL(curl -H "my-header: foo, bar=baz" https://example.com) ==
                %Req.Request{
                  url: URI.parse("https://example.com"),
-                 headers: %{"my-header" => ["foo, bar=baz"]}
+                 headers: %{"my-header" => ["foo, bar=baz"]},
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
 
@@ -371,7 +429,7 @@ defmodule CurlReqTest do
                %Req.Request{
                  method: :post,
                  url: URI.parse("https://example.com/rest/v1/leads/submitForm.json"),
-                 registered_options: MapSet.new([:compressed, :auth, :json]),
+                 registered_options: MapSet.new([:compressed, :auth, :json, :connect_options]),
                  options: %{
                    compressed: true,
                    auth: {:bearer, "6e8f18e6-141b-4d12-8397-7e7791d92ed4:lon"},
@@ -394,7 +452,8 @@ defmodule CurlReqTest do
                          }
                        }
                      ]
-                   }
+                   },
+                   connect_options: [protocols: [:http1, :http2]]
                  },
                  current_request_steps: [:compressed, :auth, :encode_body],
                  request_steps: [
@@ -409,7 +468,11 @@ defmodule CurlReqTest do
       assert ~CURL(http://example.com) ==
                %Req.Request{
                  method: :get,
-                 url: URI.parse("http://example.com")
+                 url: URI.parse("http://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
 
@@ -417,13 +480,21 @@ defmodule CurlReqTest do
       assert ~CURL(http://example.com -b "name1=value1") ==
                %Req.Request{
                  url: URI.parse("http://example.com"),
-                 headers: %{"cookie" => ["name1=value1"]}
+                 headers: %{"cookie" => ["name1=value1"]},
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
 
       assert ~CURL(http://example.com -b "name1=value1; name2=value2") ==
                %Req.Request{
                  url: URI.parse("http://example.com"),
-                 headers: %{"cookie" => ["name1=value1; name2=value2"]}
+                 headers: %{"cookie" => ["name1=value1; name2=value2"]},
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
 
@@ -432,8 +503,11 @@ defmodule CurlReqTest do
                %Req.Request{
                  url: URI.parse("http://example.com"),
                  body: nil,
-                 registered_options: MapSet.new([:form]),
-                 options: %{form: %{"name" => "foo", "mail" => "bar"}},
+                 registered_options: MapSet.new([:form, :connect_options]),
+                 options: %{
+                   form: %{"name" => "foo", "mail" => "bar"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1]
                }
@@ -451,8 +525,11 @@ defmodule CurlReqTest do
                  method: :post,
                  url: URI.parse("https://example.com/graphql"),
                  headers: %{"accept" => ["application/graphql-response+json"]},
-                 registered_options: MapSet.new([:json]),
-                 options: %{json: %{"operationName" => "get", "query" => "query get {name}"}},
+                 registered_options: MapSet.new([:json, :connect_options]),
+                 options: %{
+                   json: %{"operationName" => "get", "query" => "query get {name}"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1]
                }
@@ -470,8 +547,11 @@ defmodule CurlReqTest do
                  method: :patch,
                  url: URI.parse("https://example.com/employees/107"),
                  headers: %{"accept" => ["application/vnd.api+json"]},
-                 registered_options: MapSet.new([:json]),
-                 options: %{json: %{"data" => %{"attributes" => %{"first-name" => "Adam"}}}},
+                 registered_options: MapSet.new([:json, :connect_options]),
+                 options: %{
+                   json: %{"data" => %{"attributes" => %{"first-name" => "Adam"}}},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
                  current_request_steps: [:encode_body],
                  request_steps: [encode_body: &Req.Steps.encode_body/1]
                }
@@ -482,8 +562,11 @@ defmodule CurlReqTest do
                %Req.Request{
                  url: URI.parse("http://example.com"),
                  body: nil,
-                 registered_options: MapSet.new([:auth]),
-                 options: %{auth: {:basic, "user:pass"}},
+                 registered_options: MapSet.new([:auth, :connect_options]),
+                 options: %{
+                   auth: {:basic, "user:pass"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
                  current_request_steps: [:auth],
                  request_steps: [auth: &Req.Steps.auth/1]
                }
@@ -506,8 +589,12 @@ defmodule CurlReqTest do
                    "accept" => ["application/vnd.github+json"],
                    "x-github-api-version" => ["2022-11-28"]
                  },
-                 registered_options: MapSet.new([:auth, :redirect]),
-                 options: %{auth: {:bearer, "<YOUR-TOKEN>"}, redirect: true},
+                 registered_options: MapSet.new([:auth, :redirect, :connect_options]),
+                 options: %{
+                   auth: {:bearer, "<YOUR-TOKEN>"},
+                   redirect: true,
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
                  current_request_steps: [:auth],
                  request_steps: [auth: &Req.Steps.auth/1],
                  response_steps: [redirect: &Req.Steps.redirect/1]
@@ -519,8 +606,8 @@ defmodule CurlReqTest do
                %Req.Request{
                  url: URI.parse("http://example.com"),
                  body: nil,
-                 registered_options: MapSet.new([:compressed]),
-                 options: %{compressed: true},
+                 registered_options: MapSet.new([:compressed, :connect_options]),
+                 options: %{compressed: true, connect_options: [protocols: [:http1, :http2]]},
                  current_request_steps: [:compressed],
                  request_steps: [compressed: &Req.Steps.compressed/1]
                }
@@ -530,8 +617,8 @@ defmodule CurlReqTest do
       assert ~CURL(curl -L http://example.com) ==
                %Req.Request{
                  url: URI.parse("http://example.com"),
-                 registered_options: MapSet.new([:redirect]),
-                 options: %{redirect: true},
+                 registered_options: MapSet.new([:redirect, :connect_options]),
+                 options: %{redirect: true, connect_options: [protocols: [:http1, :http2]]},
                  response_steps: [redirect: &Req.Steps.redirect/1]
                }
     end
@@ -542,8 +629,13 @@ defmodule CurlReqTest do
                  url: URI.parse("http://example.com"),
                  headers: %{"cookie" => ["name=bar"]},
                  current_request_steps: [:auth, :encode_body],
-                 registered_options: MapSet.new([:redirect, :auth, :form]),
-                 options: %{redirect: true, auth: {:basic, "user:pass"}, form: %{"name" => "foo"}},
+                 registered_options: MapSet.new([:redirect, :auth, :form, :connect_options]),
+                 options: %{
+                   redirect: true,
+                   auth: {:basic, "user:pass"},
+                   form: %{"name" => "foo"},
+                   connect_options: [protocols: [:http1, :http2]]
+                 },
                  request_steps: [auth: &Req.Steps.auth/1, encode_body: &Req.Steps.encode_body/1],
                  response_steps: [redirect: &Req.Steps.redirect/1]
                }
@@ -555,7 +647,10 @@ defmodule CurlReqTest do
                  url: URI.parse("http://example.com"),
                  registered_options: MapSet.new([:connect_options]),
                  options: %{
-                   connect_options: [proxy: {:http, "my.proxy.com", 22225, []}]
+                   connect_options: [
+                     proxy: {:http, "my.proxy.com", 22225, []},
+                     protocols: [:http1, :http2]
+                   ]
                  }
                }
     end
@@ -570,7 +665,8 @@ defmodule CurlReqTest do
                      proxy: {:https, "my.proxy.com", 22225, []},
                      proxy_headers: [
                        {"proxy-authorization", "Basic " <> Base.encode64("foo:bar")}
-                     ]
+                     ],
+                     protocols: [:http1, :http2]
                    ]
                  }
                }
@@ -586,7 +682,8 @@ defmodule CurlReqTest do
                      proxy: {:https, "my.proxy.com", 22225, []},
                      proxy_headers: [
                        {"proxy-authorization", "Basic " <> Base.encode64("foo:bar")}
-                     ]
+                     ],
+                     protocols: [:http1, :http2]
                    ]
                  }
                }
@@ -609,7 +706,8 @@ defmodule CurlReqTest do
                  registered_options: MapSet.new([:connect_options]),
                  options: %{
                    connect_options: [
-                     transport_opts: [verify: :verify_none]
+                     transport_opts: [verify: :verify_none],
+                     protocols: [:http1, :http2]
                    ]
                  }
                }
@@ -620,7 +718,8 @@ defmodule CurlReqTest do
                  registered_options: MapSet.new([:connect_options]),
                  options: %{
                    connect_options: [
-                     transport_opts: [verify: :verify_none]
+                     transport_opts: [verify: :verify_none],
+                     protocols: [:http1, :http2]
                    ]
                  }
                }
@@ -634,7 +733,8 @@ defmodule CurlReqTest do
                  options: %{
                    connect_options: [
                      proxy: {:http, "my.proxy.com", 2233, []},
-                     transport_opts: [verify: :verify_none]
+                     transport_opts: [verify: :verify_none],
+                     protocols: [:http1, :http2]
                    ]
                  }
                }
@@ -644,13 +744,51 @@ defmodule CurlReqTest do
       assert ~CURL(curl -A "some_user_agent/0.1.0" http://example.com) ==
                %Req.Request{
                  url: URI.parse("http://example.com"),
-                 headers: %{"user-agent" => ["some_user_agent/0.1.0"]}
+                 headers: %{"user-agent" => ["some_user_agent/0.1.0"]},
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
 
       assert ~CURL(curl --user-agent "some_user_agent/0.1.0" http://example.com) ==
                %Req.Request{
                  url: URI.parse("http://example.com"),
-                 headers: %{"user-agent" => ["some_user_agent/0.1.0"]}
+                 headers: %{"user-agent" => ["some_user_agent/0.1.0"]},
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
+               }
+    end
+
+    test "protocols flag" do
+      assert ~CURL(curl --http1.0 -X GET http://example.com) ==
+               %Req.Request{
+                 url: URI.parse("http://example.com")
+               }
+
+      assert ~CURL(curl --http1.1 -X GET http://example.com) ==
+               %Req.Request{
+                 url: URI.parse("http://example.com")
+               }
+
+      assert ~CURL(curl --http2 -X GET http://example.com) ==
+               %Req.Request{
+                 url: URI.parse("http://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
+               }
+
+      assert ~CURL(curl --http1.1 --http2 -X GET http://example.com) ==
+               %Req.Request{
+                 url: URI.parse("http://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [protocols: [:http1, :http2]]
+                 }
                }
     end
   end
@@ -666,8 +804,8 @@ defmodule CurlReqTest do
       assert curl == %Req.Request{
                method: :post,
                url: URI.parse("https://example.com"),
-               registered_options: MapSet.new([:redirect]),
-               options: %{redirect: true},
+               registered_options: MapSet.new([:redirect, :connect_options]),
+               options: %{redirect: true, connect_options: [protocols: [:http1, :http2]]},
                response_steps: [redirect: &Req.Steps.redirect/1]
              }
     end
@@ -683,8 +821,8 @@ defmodule CurlReqTest do
       assert curl == %Req.Request{
                method: :post,
                url: URI.parse("https://example.com"),
-               registered_options: MapSet.new([:redirect]),
-               options: %{redirect: true},
+               registered_options: MapSet.new([:redirect, :connect_options]),
+               options: %{redirect: true, connect_options: [protocols: [:http1, :http2]]},
                response_steps: [redirect: &Req.Steps.redirect/1]
              }
     end