fetch icon indicating copy to clipboard operation
fetch copied to clipboard

Can't get form submission to work properly

Open bianchidotdev opened this issue 7 months ago • 1 comments

Hey there, I was just trying out gleam for the first time, so this is almost certainly user error, but I'm struggling to understand how to make a simple form-based POST request. It seems like the request gets modified in some interesting ways that's making a client_credentials oauth token request fail (specifically error_description": "invalid_request, Missing grant_type parameter value").

I'm interested in using gleam with bun so I could theoretically create a fairly static CLI driven binary, so that's why I'm not using the erlang target in this scenario.

System context:

  • Mac 15.4.1
  • gleam 1.10.0
  • bun 1.2.9

I'm using httpbin.org for testing to get a response back with what I sent it and it seems like the form components are getting put under name maybe.

Here's my gleam code:

pub fn send_form() {
  let id = "test"
  let secret = "testsecret"
  let assert Ok(token_req) = request.to("https://httpbin.org/post")

  let creds =
    bit_array.from_string(id <> ":" <> secret)
    |> bit_array.base64_encode(True)

  let payload_form =
    form_data.new()
    |> form_data.set("grant_type", "client_credentials")

  let token_req =
    token_req
    |> request.set_method(http.Post)
    |> request.set_header("Content-Type", "application/x-www-form-urlencoded")
    |> request.set_header("Authorization", "Basic " <> creds)
    |> request.set_body(payload_form)

  use resp <- promise.try_await(fetch.send_form_data(token_req))
  use body <- promise.try_await(fetch.read_json_body(resp))

  echo body
  promise.resolve(Ok(Nil))
}

Response:

//js({ "args": //js({}), "data": "", "files": //js({}), "form": //js({ "---WebkitFormBoundary8d65f103a3d34404be041b33c84280fa\r\nContent-Disposition: form-data; name": "\"grant_type\"\r\n\r\nclient_credentials\r\n---WebkitFormBoundary8d65f103a3d34404be041b33c84280fa--\r\n" }), "headers": //js({ "Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Authorization": "Basic dGVzdDp0ZXN0c2VjcmV0", "Content-Length": "185", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "Bun/1.2.9", "X-Amzn-Trace-Id": "Root=1-681114b3-1ee85fdb0456e9912b353ec0" }), "json": //js(null), "origin": "198.44.129.120", "url": "https://httpbin.org/post" })

# the important bit seems to be this:
name": "\"grant_type\"\r\n\r\nclient_credentials\r\n---WebkitFormBoundary8d65f103a3d34404be041b33c84280fa--\r\n"

Here's some elixir code that works:

Mix.install([:req])
url = "https://httpbin.org/post"
form = [grant_type: "client_credentials"]
creds_string = "test:testsecret"
Req.post(url, form: form, auth: {:basic, creds_string})

#> {:ok,
 %Req.Response{
   status: 200,
   headers: %{
     "access-control-allow-credentials" => ["true"],
     "access-control-allow-origin" => ["*"],
     "connection" => ["keep-alive"],
     "content-type" => ["application/json"],
     "date" => ["Tue, 29 Apr 2025 17:59:52 GMT"],
     "server" => ["gunicorn/19.9.0"]
   },
   body: %{
     "args" => %{},
     "data" => "",
     "files" => %{},
     "form" => %{"grant_type" => "client_credentials"},
     "headers" => %{
       "Accept-Encoding" => "gzip",
       "Authorization" => "Basic dGVzdDp0ZXN0c2VjcmV0",
       "Content-Length" => "29",
       "Content-Type" => "application/x-www-form-urlencoded",
       "Host" => "httpbin.org",
       "User-Agent" => "req/0.5.10",
       "X-Amzn-Trace-Id" => "Root=1-68111397-3d5c1061652553126eb83cff"
     },
     "json" => nil,
     "origin" => "198.44.129.120",
     "url" => "https://httpbin.org/post"
   },
   trailers: %{},
   private: %{}
 }}

bianchidotdev avatar Apr 29 '25 18:04 bianchidotdev

The first thing I see is that you've set a formencoded content type, but the body is a multipart form. I think that fetch may overwrite the incorrect content type though.

Could you share the request received for each please. I couldn't guess what the difference is and I don't know the relationship between the JavaScript object you've shared and what request fetch will send for it. Thank you

lpil avatar Apr 30 '25 13:04 lpil