tesla icon indicating copy to clipboard operation
tesla copied to clipboard

Same request fails with some adapters and succeeds with others

Open gaiabeatrice opened this issue 5 years ago • 5 comments

Hello, I am migrating some of our requests from HTTPoison to Tesla and I found some requests are failing if I use httpc or Mint as the adapters, while Hackney works. I understand that it is because of HTTPoison using Hackney under the hood.

It would be nice, however to see some sort of uniformity in handling requests so that switching to a new adapter can be as seamless as possible.

uri = <URI>
method = :post
body = nil
headers: [
  {"Content-type", "application/json"},
  {"Accept", "application/json"},
  {"Authorization",
   "Bearer <TOKEN>"}
]
query = []

Tesla.request(client(),
   method: method,
   url: uri,
   body: body,
   headers: headers,
   query: query
)

defp client() do
    Tesla.client([Tesla.Middleware.JSON])
end

If I run that with Hackney it works well, if I use httpc for example I get this error

** (FunctionClauseError) no function clause matching in :httpc.request/5

    The following arguments were given to :httpc.request/5:

        # 1
        :post

        # 2
        {'<URL>',
         [
           {'Content-type', 'application/json'},
           {'Accept', 'application/json'},
           {'Authorization',
            'Bearer <TOKEN>'}
         ]}

        # 3
        [autoredirect: false]

        # 4
        []

        # 5
        :default

    (inets) httpc.erl:149: :httpc.request/5
    (tesla) lib/tesla/adapter/httpc.ex:52: Tesla.Adapter.Httpc.request/2
    (tesla) lib/tesla/adapter/httpc.ex:22: Tesla.Adapter.Httpc.call/2
    (tesla) lib/tesla/middleware/json.ex:54: Tesla.Middleware.JSON.call/3

gaiabeatrice avatar May 28 '20 21:05 gaiabeatrice

Seems like an issue with adapters not handling POST request with nil body.

@gaiabeatrice Could you provide a test case in https://github.com/teamon/tesla/blob/master/test/support/adapter_case/basic.ex ?

teamon avatar Jun 15 '20 08:06 teamon

  • Httpc does not accept nil as body. See http://www1.erlang.org/doc/man/httpc.html
  • Tesla uses any for body: https://hexdocs.pm/tesla/Tesla.Env.html#t:body/0
  • Hackney accepts term (any) for body: https://hexdocs.pm/hackney/hackney.html#request-5

You can just pass empty string ("").

{:ok, _} =
  Tesla.client([Tesla.Middleware.JSON], Tesla.Adapter.Httpc)
  |> Tesla.post("https://httpbin.org/post", "")

We may handle the nil value at Tesla.Adapter.Httpc as it's easy to handle - but at the same time I'm not sure how much we have to make adapter behave exactly same way, since at some level it's not possible at all.

chulkilee avatar Jun 16 '20 04:06 chulkilee

I think it makes sense to:

  • [ ] Add a basic adapter test case to sending POST request with nil body
  • [ ] Handle nil body for adapters that fail

teamon avatar Dec 17 '21 14:12 teamon

Hey @teamon I'm now to tesla but would like to help on this issue. I'll try to first create the test that fails. Any other guidance?

fbergero avatar May 06 '22 22:05 fbergero

I've just seen that @gaiabeatrice has open a PR for this so I'll find some other issue to work on

fbergero avatar May 06 '22 22:05 fbergero

I couldn't test :gun, I kept getting the following error:

iex(2)> {:ok, _} = Application.ensure_all_started(:gun)
** (MatchError) no match of right hand side value: {:error, {:cowlib, {'no such file or directory', 'cowlib.app'}}}

But, all the other adapters support passing nil body as far as I can tell:

{:ok, _} = Application.ensure_all_started(:hackney)
# {:ok, _} = Application.ensure_all_started(:gun)
{:ok, _} = :ibrowse.start()
{:ok, _} = Finch.start_link(name: MyFinch)

adapters = [
  {Tesla.Adapter.Httpc, []},
  {Tesla.Adapter.Hackney, []},
  {Tesla.Adapter.Mint, []},
  {Tesla.Adapter.Ibrowse, []},
  {Tesla.Adapter.Finch, [name: MyFinch]},
  # {Tesla.Adapter.Gun, []},
]

[nil, nil, nil, nil, nil] = for adapter <- adapters do
  client = Tesla.client([Tesla.Middleware.JSON], adapter)
  {:ok, res} = Tesla.post(client, "https://httpbin.org/post", nil)
  nil = res.body["json"]
end
uri = "https://httpbin.org/post"
method = :post
body = nil
headers = [
  {"Content-type", "application/json"},
  {"Accept", "application/json"},
  {"Authorization",
    "Bearer <TOKEN>"}
]
query = []
{:ok, _} = Tesla.request(Tesla.client([Tesla.Middleware.JSON]),
  method: method,
  url: uri,
  body: body,
  headers: headers,
  query: query
)

I will close this. If you face an issue with :gun package, please open a new issue, but as far as I can tell. It is working.

yordis avatar Sep 10 '23 12:09 yordis