Handling parsing body errors gracefully

I’ve implemented a module implementing the behaviour of Plug.Parsers

From the doc I followed, my parse/5 callback returns {:ok, params, conn} when the body has been successfully parsed.

But when there is a parsing error in the body, I think I have to return {:next, conn} so that Plug can go on to the next parsers.

When this happens, I have errors in my logs and the clients gets a 500 error, I would expect him to get a 415 or a 400.
Obviously I need to add something in my code to handle gracefully Plug.Parsers.UnsupportedMediaTypeError

Where should I put this ?

Here is an extract of my parse function:

require Logger
require Plug.Parsers.ParseError

defmodule Wsdataselect.BodyParser.ParseError do
  defexception message: "Malformed body"
end

defmodule Wsdataselect.BodyParser do
  @moduledoc """
  This modules parses the body of a HTTP request to generate a %Wsdataselect.DataRequest{} structure.
  """
  alias Wsdataselect.{DataRequest, Stream}
  @behaviour Plug.Parsers

  def init(opts), do: opts

  @doc """
  Parse the body of the request in order to return a %Wsdataselect.DataRequest{}
  This function implements the behaviour of Plug.Parsers.
  """
  @spec parse(Plug.Conn.t(), any, any, any, any) :: {:ok, DataRequest.t(), Plug.Conn.t()} | {:error, :too_large, Plug.Conn.t()} | {:next, Plug.Conn.t()}
  def parse(conn, _, _, _, _) do
    {:ok, body, conn} = Plug.Conn.read_body(conn)
    body_list = String.split(body, "\n", trim: true)
    if length(body_list) > Application.get_env(:wsdataselect, :post_max_lines) do
      {:error, :too_large, conn}
    else
      case Enum.reduce(body_list,  %DataRequest{}, &parse_line/2) do
        {:error, m} ->
          Logger.error(m)
          {:next, conn}
        datareq ->
          Logger.debug("Body parsed: #{inspect(datareq)}")
          {:ok, datareq, conn}
      end
    end
  end

And Here my router:

defmodule Wsdataselect.Router do
  @moduledoc """
  This module is the entrypoint of HTT requests from the user. It uses Plug to manage the requests.
  """
  require Logger
  require DateTime
  alias Wsdataselect.Controller
  use Plug.Router
  use Plug.ErrorHandler
  use Plug.Builder

  plug Plug.Logger
  plug Plug.Static,
    at: "/",
    from: {:wsdataselect, "priv/static"},
    content_types: %{"application.wadl" => "text/xml; charset=utf-8", "index.html" => "text/html"}
  plug Plug.RequestId
  plug :match
  plug Plug.Parsers, parsers: [Wsdataselect.BodyParser]
  plug :dispatch

  post "/query" do
    Logger.debug("Body: #{inspect(conn.body_params)}")
    conn
    |> Controller.post_query()
  end

When a body with wrong format is POSTed, the logs are:

16:07:57.070 request_id=F4B8-uvNBI9DfjkAAACE [error] GenServer #PID<0.637.0> terminating
** (Plug.Parsers.UnsupportedMediaTypeError) unsupported media type application/json

Where in my code should I put a Plug.Conn.send_resp(conn, 400, "Malformed body") ?

I’ve added this to my router:

  @impl Plug.ErrorHandler
  def handle_errors(conn, %{kind: _k, reason: r, stack: _stack}) do
    send_resp(conn, conn.status, r.exception.message)
  end

And this line in my Parser:

raise %Plug.Parsers.ParseError{exception: %Wsdataselect.BodyParser.ParseError{message: m}}

Which makes a nice response to the client, but still crashes on the server side:

16:33:03.830 request_id=F4B-Wb0PupyhAfsAAAFj [error] GenServer #PID<0.626.0> terminating
** (Plug.Parsers.ParseError)

sorry for asking, but why exactly did you chose plug(router) instead of like phoenix?

The project is a webservice with no real API, no CRUD operations, therefore I have no need for a large framework and can keep things small. I might be wrong because it’s my first Elixir project, but I did a lot of Ruby on Rails and I feel like Phoenix is somewhat the equivalent ?

Well, I thought the same but Phoenix is nothing bloat or something you CANT use for such a small project. Even for the smallest ones, you can easily remove all the html and other integrated parts like the dashboard, ecto (database) with the --no-XYZ command at the beginning and you have the api only part. very cool imo and luck enough someone recommended that for me