Background
I have a simple Plug router with a PUT endpoint that receives a JSON body. To parse it I am using Plug.Parsers.
Problem
The Plug.Parsers
plug works fine and puts the json inside conn.body_params
. However, if the JSON I am receiving is malformated, my application explodes with errors. To prevent this I am using Plug.ErrorHandler but since it re-raises the error after, the app still explodes.
Code
This is my router.
defmodule Api do
use Plug.{Router, ErrorHandler}
alias Api.Controllers.{Products, NotFound}
plug Plug.Logger
plug :match
plug Plug.Parsers,
parsers: [:urlencoded, :json],
pass: ["text/*"],
json_decoder: Jason
plug :dispatch
put "/products", do: Products.process(conn)
match _, do: NotFound.process(conn)
def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
send_resp(conn, conn.status, "Something went wrong")
end
end
It should be noted that in reality Products.process
is not (or should not be) called because Plug.Parsers
raises before.
And this is my test:
test "returns 400 when the request is not a valid JSON" do
# Arrange
body_params = "" # this is not valid JSON
conn =
:put
|> conn("/products", body_params)
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
# Act
conn = Api.call(conn, Api.init([]))
# Assert
assert conn.state == :sent
assert conn.status == 400
assert conn.resp_body == "Invalid JSON in body request"
end
Error
As you can probably guess, I am expecting the request to return 400 and a nice error message. Instead I get this:
test PUT /products returns 400 when the request is not a valid JSON (ApiTest)
test/api_test.exs:143
** (Plug.Conn.WrapperError) ** (FunctionClauseError) no function clause matching in Api.Controllers.Products.handle_flow_response/2
code: conn = Api.call(conn, @opts)
stacktrace:
(api 0.1.0) lib/api/controllers/products.ex:39: Api.Controllers.Products.handle_flow_response(:ok, %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :…}, assigns: %{}, before_send: [#Function<1.129014997/1 in Plug.Logger.call/2>], body_params: %{}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: “www.example.com”, method: “PUT”, owner: #PID<0.406.0>, params: %{}, path_info: [“cars”], path_params: %{}, port: 80, private: %{plug_route: {“/products”, #Function<1.102964628/2 in Api.do_match/4>}}, query_params: %{}, query_string: “”, remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{“accept”, “application/json”}, {“content-type”, “application/json”}], request_path: “/products”, resp_body: nil, resp_cookies: %{}, resp_headers: [{“cache-control”, “max-age=0, private, must-revalidate”}], scheme: :http, script_name: , secret_key_base: nil, state: :unset, status: nil})
(api 0.1.0) lib/plug/router.ex:284: Api.dispatch/2
(api 0.1.0) lib/api.ex:1: Api.plug_builder_call/2
(api 0.1.0) lib/plug/error_handler.ex:65: Api.call/2
test/api_test.exs:154: (test)
I am rather dumbfounded.
Failed Fix
To avoid this I tried modifying the handle_errors
function to the following, but it still failed:
def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
send_resp(conn, conn.status, "Something went wrong")
{:error, :something_went_wrong}
end
No matter what I do, the code always flows with “:ok”. Nothing I do seems to have control over the error.
Question
How can I prevent this error from re-raising and simply return the nice error message I have in my test?