jsonelixirplug

Prevent Plug.ErrorHandler from re-raising error after callback


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 = "[{\"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

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 /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.

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

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?


Solution

  • 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", "")