Repository: swoosh/swoosh (elixir)

Issue:
Mailgun provider is not compatible with Finch API client
### Describe the bug

If you replace hackney with finch the Mailgun provider will stop sending the request body. Here are two requests logged, the one with empty body is the finch and other one is hackney:
<img width="1728" alt="Screenshot 2024-03-18 at 10 36 42" src="https://github.com/swoosh/swoosh/assets/1877644/ba7223bd-66df-4166-aa7d-0101e3b8f70e">

My best guess is that Finch sees the `Content-Length: 0` request header and either discards the body or the end server just doesn't read it because of the header, while Hackney will actually ignore the header you would set manually and re-calculate it. The header is set for both API clients:
```
hackney_headers = [
   {"User-Agent", "swoosh/1.16.1"},
   {"Content-Type", "application/x-www-form-urlencoded"},
   {"Content-Length", "0"},
   {"User-Agent", "swoosh/1.16.1"},
   {"Accept", "*/*"},
   {"Authorization",
    "Basic REDACTED"}
 ]

finch_headers = [
    {"User-Agent", "swoosh/1.16.1"},
    {"Content-Type", "application/x-www-form-urlencoded"},
    {"Content-Length", "0"},
    {"User-Agent", "swoosh/1.16.1"},
    {"Accept", "*/*"},
    {"Authorization",
     "Basic REDACTED"}
]
```
and HTTP bin received a proper value in case of Hackney.

### Steps to Reproduce the Bug or Issue

add `config :swoosh, :api_client, Swoosh.ApiClient.Finch` to the config

### Expected behavior

_No response_

### Your reproducible example

_No response_

### Screenshots or Videos

_No response_

### Platform

- OTP:
- Elixir:


### Additional context

_No response_

Pull request:
Fix Mailgun adapter incompatibility with Finch
Closes #881

Patch:
diff --git a/lib/swoosh/adapters/mailgun.ex b/lib/swoosh/adapters/mailgun.ex
index fee76d3..6732be8 100644
--- a/lib/swoosh/adapters/mailgun.ex
+++ b/lib/swoosh/adapters/mailgun.ex
@@ -248,7 +248,8 @@ defmodule Swoosh.Adapters.Mailgun do
 
   defp prepare_template_options(body, _), do: body
 
-  defp encode_body(%{attachments: attachments} = params) do
+  @doc false
+  def encode_body(%{attachments: attachments} = params) do
     attachments
     |> Enum.reduce(
       params
@@ -272,8 +273,10 @@ defmodule Swoosh.Adapters.Mailgun do
     )
   end
 
-  defp encode_body(no_attachments),
-    do: {"application/x-www-form-urlencoded", 0, Plug.Conn.Query.encode(no_attachments)}
+  def encode_body(no_attachments) do
+    body = Plug.Conn.Query.encode(no_attachments)
+    {"application/x-www-form-urlencoded", byte_size(body), body}
+  end
 
   defp encode_variable(var) when is_map(var) or is_list(var),
     do: Swoosh.json_library().encode!(var)


Test patch:
diff --git a/test/swoosh/adapters/mailgun_test.exs b/test/swoosh/adapters/mailgun_test.exs
index f746075..fe4d85b 100644
--- a/test/swoosh/adapters/mailgun_test.exs
+++ b/test/swoosh/adapters/mailgun_test.exs
@@ -46,6 +46,10 @@ defmodule Swoosh.Adapters.MailgunTest do
       assert expected_path == conn.request_path
       assert "POST" == conn.method
 
+      assert {"content-type", "application/x-www-form-urlencoded"} in conn.req_headers
+      assert {"content-length", "118"} in conn.req_headers
+      assert {"authorization", "Basic YXBpOmZha2U="} in conn.req_headers
+
       Plug.Conn.resp(conn, 200, @success_response)
     end)
 
@@ -131,6 +135,11 @@ defmodule Swoosh.Adapters.MailgunTest do
       assert expected_path == conn.request_path
       assert "POST" == conn.method
 
+      content_type = List.keyfind(conn.req_headers, "content-type", 0)
+      assert {"content-type", "multipart/form-data; boundary=" <> _} = content_type
+      assert {"content-length", "19503"} in conn.req_headers
+      assert {"authorization", "Basic YXBpOmZha2U="} in conn.req_headers
+
       Plug.Conn.resp(conn, 200, @success_response)
     end)
 
@@ -424,4 +433,31 @@ defmodule Swoosh.Adapters.MailgunTest do
                    Mailgun.validate_config([])
                  end
   end
+
+  test "encode_body/1 sets valid content length" do
+    params = %{
+      to: "\"Steve Rogers\" <steve.rogers@example.com>",
+      from: "\"T Stark\" <tony.stark@example.com>",
+      subject: "Hello, Avengers!",
+      html: "<h1>Hello</h1>",
+      text: "Hello"
+    }
+
+    assert {"application/x-www-form-urlencoded", 174, _body} = Mailgun.encode_body(params)
+  end
+
+  test "encode_body/1 sets valid content length for attachments" do
+    params = %{
+      to: "\"Steve Rogers\" <steve.rogers@example.com>",
+      from: "\"T Stark\" <tony.stark@example.com>",
+      subject: "Hello, Avengers!",
+      html: "<h1>Hello</h1>",
+      text: "Hello",
+      attachments: [
+        Multipart.Part.binary_body("foo")
+      ]
+    }
+
+    assert {"multipart/form-data; boundary=" <> _, 647, _body} = Mailgun.encode_body(params)
+  end
 end