Repository: derekkraan/curl_req
Pull request #41 (2025-01-14 10:27:12)

Fix parsing of header paramters for all headers
A requeset with

```
curl https:example.com -H 'Content-Type: application/json; charset=utf-8'
```

would've failed because we didn't pattern match on the header with parameters. This is now fixed. We also raise if we can't parse the json instead of a fallback to an empty map. This should make it more transparent to the user because the request would've failed anyway


Patch:
diff --git a/.iex.exs b/.iex.exs
new file mode 100644
index 0000000..340ff2a
--- /dev/null
+++ b/.iex.exs
@@ -0,0 +1,4 @@
+alias CurlReq.Request
+
+import CurlReq
+import CurlReq.Request
diff --git a/lib/curl_req/curl.ex b/lib/curl_req/curl.ex
index aed4cae..a7eb5fd 100644
--- a/lib/curl_req/curl.ex
+++ b/lib/curl_req/curl.ex
@@ -286,7 +286,7 @@ defmodule CurlReq.Curl do
     headers =
       for {key, values} <- request.headers, reduce: [] do
         headers ->
-          [headers, header_flag(flag_style, [key, ": ", Enum.intersperse(values, ";")])]
+          [headers, header_flag(flag_style, [key, ": ", Enum.intersperse(values, "; ")])]
       end
 
     headers =
diff --git a/lib/curl_req/req.ex b/lib/curl_req/req.ex
index 4261c97..3db24f7 100644
--- a/lib/curl_req/req.ex
+++ b/lib/curl_req/req.ex
@@ -76,11 +76,6 @@ defmodule CurlReq.Req do
       |> Req.merge(url: request.url)
       |> Req.merge(method: request.method)
 
-    cookies =
-      request.cookies
-      |> Enum.map(fn {key, val} -> "#{key}=#{val}" end)
-      |> Enum.join(";")
-
     req =
       case request.user_agent do
         :req -> req
@@ -151,12 +146,13 @@ defmodule CurlReq.Req do
         req -> Req.Request.put_header(req, key, values)
       end
 
-    req =
-      if cookies != "" do
-        Req.Request.put_header(req, "cookie", cookies)
-      else
-        req
-      end
+    cookies =
+      request.cookies
+      |> Enum.map(fn {key, val} ->
+        IO.iodata_to_binary([key, "=", val])
+      end)
+
+    req = if cookies != [], do: Req.Request.put_header(req, "cookie", cookies), else: req
 
     proxy =
       if request.proxy do
diff --git a/lib/curl_req/request.ex b/lib/curl_req/request.ex
index 22b82eb..38e68e3 100644
--- a/lib/curl_req/request.ex
+++ b/lib/curl_req/request.ex
@@ -71,45 +71,47 @@ defmodule CurlReq.Request do
       {:bearer, "foobar"}
   """
   @spec put_header(__MODULE__.t(), String.t(), String.t() | [String.t()]) :: __MODULE__.t()
-  def put_header(%__MODULE__{} = request, key, [val]) do
+  def put_header(%__MODULE__{} = request, key, val) when is_binary(val) do
+    key = String.downcase(key) |> String.trim()
+    val = String.split(val, ";", trim: true) |> Enum.map(&String.trim/1)
     put_header(request, key, val)
   end
 
-  def put_header(%__MODULE__{} = request, key, val) when is_binary(val) do
-    key = String.downcase(key)
+  def put_header(%__MODULE__{} = request, key, val) when is_list(val) do
+    key = String.downcase(key) |> String.trim()
 
     case {key, val} do
-      {"authorization", "Bearer " <> token} ->
+      {"authorization", ["Bearer " <> token | _]} ->
         %{request | auth: {:bearer, token}}
 
-      {"authorization", "Basic " <> userinfo} ->
+      {"authorization", ["Basic " <> userinfo | _]} ->
         %{request | auth: {:basic, userinfo}}
 
-      {"accept-encoding", compression}
+      {"accept-encoding", [compression | _]}
       when compression in ["gzip", "br", "zstd"] ->
         put_compression(request, String.to_existing_atom(compression))
 
-      {"content-type", "application/json"} ->
+      {"content-type", ["application/json" | _]} ->
         %{request | encoding: :json}
 
-      {"content-type", "application/vnd.api+json"} ->
+      {"content-type", ["application/vnd.api+json" | _]} ->
         %{request | encoding: :json}
 
-      {"content-type", "application/x-www-form-urlencoded"} ->
+      {"content-type", ["application/x-www-form-urlencoded" | _]} ->
         %{request | encoding: :form}
 
-      {"user-agent", user_agent} ->
+      {"user-agent", [user_agent | _]} ->
         put_user_agent(request, user_agent)
 
       {"cookie", cookies} ->
-        for cookie <- String.split(cookies, ";"), reduce: request do
+        for cookie <- cookies, reduce: request do
           request ->
             [key, value] = String.split(cookie, "=")
             put_cookie(request, key, value)
         end
 
       {key, val} ->
-        headers = Map.put(request.headers, key, String.split(val, ";"))
+        headers = Map.put(request.headers, key, val)
         %{request | headers: headers}
     end
   end
@@ -182,10 +184,7 @@ defmodule CurlReq.Request do
   defp decode_json(json) when is_map(json), do: json
 
   defp decode_json(input) when is_binary(input) or is_list(input) do
-    case Jason.decode(input) do
-      {:ok, json} -> json
-      _ -> %{}
-    end
+    Jason.decode!(input)
   end
 
   defp decode_form(nil), do: nil


Test patch:
diff --git a/test/curl_req_test.exs b/test/curl_req_test.exs
index bb8b539..5bbaf17 100644
--- a/test/curl_req_test.exs
+++ b/test/curl_req_test.exs
@@ -83,6 +83,18 @@ defmodule CurlReqTest do
                |> CurlReq.to_curl()
     end
 
+    test "header" do
+      assert ~s(curl --compressed -H "my-header: foo" -X GET https://example.com) ==
+               Req.new(url: "https://example.com", headers: %{"my-header" => ["foo"]})
+               |> CurlReq.to_curl()
+    end
+
+    test "parameterized header" do
+      assert ~s(curl --compressed -H "my-header: foo; bar=baz" -X GET https://example.com) ==
+               Req.new(url: "https://example.com", headers: %{"my-header" => ["foo", "bar=baz"]})
+               |> CurlReq.to_curl()
+    end
+
     test "req flavor with explicit headers" do
       assert ~s|curl -H "accept-encoding: gzip" -A "req/#{req_version()}" -X GET https://example.com| ==
                Req.new(url: "https://example.com")
@@ -316,6 +328,38 @@ defmodule CurlReqTest do
                }
     end
 
+    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]),
+                 current_request_steps: [:encode_body],
+                 request_steps: [encode_body: &Req.Steps.encode_body/1],
+                 url: URI.parse("https://example.com"),
+                 method: :post
+               }
+    end
+
+    test "header with parameter" 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"]}
+               }
+    end
+
+    test "complex cookie" do
+      request =
+        ~CURL(curl --header 'Cookie: TealeafAkaSid=JA-JSAXRCLjKYhjV9IXTzYUbcV1Lnhqf; sapphire=1; visitorId=0184E4601D5A020183FFBB133 80347CE; GuestLocation=33196|25.660|-80.440|FL|US' -X GET https://example.com)
+
+      cookies = request.headers["cookie"]
+
+      assert "TealeafAkaSid=JA-JSAXRCLjKYhjV9IXTzYUbcV1Lnhqf" in cookies
+      assert "sapphire=1" in cookies
+      assert "visitorId=0184E4601D5A020183FFBB133 80347CE" in cookies
+      assert "GuestLocation=33196|25.660|-80.440|FL|US" in cookies
+    end
+
     test "multiple headers with body" do
       assert ~CURL(curl -H "accept-encoding: gzip" -H "authorization: Bearer 6e8f18e6-141b-4d12-8397-7e7791d92ed4:lon" -H "content-type: application/json" -H "user-agent: req/0.4.14" -d "{\"input\":[{\"leadFormFields\":{\"Company\":\"k\",\"Country\":\"DZ\",\"Email\":\"k\",\"FirstName\":\"k\",\"Industry\":\"CTO\",\"LastName\":\"k\",\"Phone\":\"k\",\"PostalCode\":\"1234ZZ\",\"jobspecialty\":\"engineer\",\"message\":\"I would like to know if Roche delivers to The Netherlands.\"}}],\"formId\":4318}" -X POST "https://example.com/rest/v1/leads/submitForm.json") ==
                %Req.Request{
@@ -373,7 +417,7 @@ defmodule CurlReqTest do
       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"]}
                }
     end