I have a simple Plug router with a PUT endpoint that receives a JSON body. To parse it I am using Plug.Parsers.
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.
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 = "[{\"id\": 1}" # 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
As you can probably guess, I am expecting the request to return 400 and a nice error message. Instead I get this:
test PUT /cars returns 400 when the request has invalid JSON body (ApiTest) test/api_test.exs:157 ** (Plug.Parsers.ParseError) malformed request, a Jason.DecodeError exception was raised with message “unexpected end of input at position 10” code: conn = Api.call(conn, @opts) stacktrace: (plug 1.10.4) lib/plug/parsers/json.ex:88: Plug.Parsers.JSON.decode/2 (plug 1.10.4) lib/plug/parsers.ex:313: Plug.Parsers.reduce/8 (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:168: (test)
I am rather dumbfounded.
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
Nothing I do seems to have control over the error.
How can I prevent this error from re-raising and simply return the nice error message I have in my test?
I don't think you should prevent the error from re-raising. I think the problem is that your tests are not expecting the error.
You could catch the error:
assert %Plug.Parsers.ParseError{} =
catch_error(Api.call(conn, Api.init([])))
Or you could avoid the problem by using an HTTP client to test your endpoint, rather than excercise your plug directly. For example, using Tesla:
assert %{status: 400, body: "Invalid JSON in body request"} =
Tesla.put!(@base_url <> "/products", "")