Help with HTTPoison POST

As a follow up to my earlier question:

I have the code compiling and running but not getting a successful login from the rest server. If I use cURL all is good, but my code reports a success code of 200, so the call executed correctly, but a failed login.

It seems to me that the parameters are the same, so I can’t figure out what is wrong. A successful login returns:

{
“return_code”: 1,
“session”: “nlvpneu7jgk2t4nvrus9hi4bp6”,
“uid”: “804276”,
“username”: "me@example.com"
}

But I get the return for a failed login:

{“return_code”:0}

I’ll paste the cURL and my code, if someone sees the issue and can help me I would greatly appreciate the help.

Here is the cURL:

curl -X POST
https://simplisafe.com/mobile/login?mobile=true
-H ‘Cache-Control: no-cache’
-H ‘Content-Type: application/x-www-form-urlencoded’
-d ‘name=user%example.com&pass=mypass&device_name=my_iphone&device_uuid=51644e80-1b62-11e3-b773-0800200c9a66&version=1200&no_persist=1&XDEBUG_SESSION_START=session_name’

Here is my code:

defmodule Simplisafe.SsFreeze do
@user_agent [ { “User-agent”, “Elixir” } ]

def fetch(user, password) do
ss_url()
|> ss_login(user, password)
|> handle_response
end

defp ss_url do
https://simplisafe.com/mobile/login?mobile=true
end

defp handle_response({ :ok, %{status_code: 200, body: body}}) do
{ :ok, body }
end

defp handle_response({ :error, %{status_code: sc, body: body}}) do
{ :error, sc, body }
end

defp handle_response({ ec, %{status_code: sc, body: body}}) do
{ ec, sc, body }
end

def post_body(user, password) do
%{“name” => user,
“pass” => password,
“device_name” => “my_iphone”,
“device_uuid” => “51644e80-1b62-11e3-b773-0800200c9a66”,
“version” => “1200”,
“no_persist” => “1”,
“XDEBUG_SESSION_START” => “session_name”,
} |> Poison.encode!
end

def ss_login(user, password) do
HTTPoison.post(ss_url(), post_body(user, password),
%{“Content-Type” => “application/x-www-form-urlencoded”, “Cache-Control” => “no-cache”})
end

def ss_login(url, user, password) do
HTTPoison.post(url, post_body(user, password), %{“Content-Type” => “application/x-www-form-urlencoded”, “Cache-Control” => “no-cache”})
end
end

You are creating a json object but then saying that the Content-Type is "Content-Type" => "application/x-www-form-urlencoded" … that should be application/json or else you can pass a {:form, [{K, V}, ...]} tuple to HTTPoison.post as the body to send a urlencoded form. One or the other … hth :slight_smile:

2 Likes

Thanks, I tried making that change but no luck.

According to your curl example, the request should be sent with “application/x-www-form-urlencoded” format. You can try URI.encode_query/1 to encode a map or a keyword list to that format, for example,

req_body = URI.encode_query(%{"name" => "who am i", "pass" => "$3cret"})
HTTPoison.post(
  "http://www.example.com/login",
  req_body,
  %{"Content-Type" => "application/x-www-form-urlencoded"}
)
8 Likes

That fixed it, thanks!

Does HTTPoison handle the session cookies? The docs are a little confusing but it seems that it is supposed to.

No, it doesn’t. Here is the HTTP client module I wrote for a scraper, with HTTPoison for sending requests, and Floki for HTML parsing:

defmodule EkangScraper.HttpClient do
  alias EkangScraper.CookieStore

  def request_raw(method, url, params) do
    payload = URI.encode_query(params)
    cookies = Agent.get(CookieStore, &(&1)) || []
    try do
      response = case method do
        method when method in [:get, :delete] -> 
          real_url = url <> "?" <> payload
          apply(HTTPoison, :"#{method}!", [
            real_url, 
            %{},
            [hackney: [cookie: cookies]]])
        _ ->
          apply(HTTPoison, :"#{method}!", [
            url, 
            payload, 
            %{"Content-Type" => "application/x-www-form-urlencoded; charset=utf-8"},
            [hackney: [cookie: cookies]]])
      end

      case response.status_code do
        code when code >= 200 and code < 400 ->
          Agent.update(CookieStore, &(cookies(response) || &1))
          response.body
        code -> raise """
          Oops! #{code}
            URL: #{url}
            Method: #{method}
            Params: #{inspect params}
            Cookies: #{inspect cookies}
        """
      end
    rescue
      e in HTTPoison.Error ->
        raise """
          Oops! #{e.reason}!
            URL: #{url}
            Method: #{method}
            Params: #{inspect params}
            Cookies: #{inspect cookies}
        """
    end
  end

  def request(method, url, params) do
    request_raw(method, url, params)
    |> Floki.parse()
  end

  for method <- [:get, :post, :patch, :put, :delete] do
    def unquote(:"#{method}_raw")(url, params \\ %{}) do
      request_raw(unquote(method), url, params)
    end

    def unquote(method)(url, params \\ %{}) do
      request(unquote(method), url, params)
    end
  end

  defp cookies(%HTTPoison.Response{} = resp) do
    resp.headers
    |> Enum.filter(fn
      {"Set-Cookie", _} -> true
      _ -> false
    end)
    |> Enum.map(fn{_, cookie} -> cookie end)
  end
end 

The CookieStore is just a name for an Agent process

# in application.ex
children = [
      worker(Agent, [fn -> nil end, [name: EkangScraper.CookieStore]])
]

How to use

# GET request. 
# Params are appended to URL as query string. 
# Cookies are automatically handled
doc = EkangScraper.HttpClient.get("http://www.example.com/path", %{foo: "bar", baz: "qux"})

# POST request. 
# Params are encoded as application/x-www-form-urlencoded and put to request body.
# Cookies are automatically handled.
doc = EkangScraper.HttpClient.post("http://www.example.com/path", %{foo: "bar", baz: "qux"})

# If you just want a raw response body as a string
body_string = EkangScraper.HttpClient.get_raw("http://www.example.com/path", %{foo: "bar", baz: "qux"})

There are patch, put, delete and their _raw counterpart as well.

2 Likes

Thanks very much, I’ll study this over the weekend.