1

I'm writing a Plug Parser that among other things decodes JSON using Poison (I'd prefer to let Plug.Parsers.JSON do that, but I need to read the raw request body to verify it against a signature, so that's not possible).

I'm using Poison.decode/2 to decode the json. This returns an {:error, ...} tuple on error. As a Plug parser, I think I am expected to raise Plug.Parsers.ParseError if there was an error during the parsing. However ParseError expects an exception struct. I don't have one of those, I only have the tuple returned from Poison.decode/2.

As a workaround, I can use use Poison.decode!/2 and rescue the raised error, re-raising it as a ParseError, but that seems weird when the non-raising decode/2 is available.

So my questions are, in increasing abstractness:

  1. How do I raise a ParseError from a parser without a source exception?
  2. Do I need to raise the ParseError, or it it better to raise my own exception?
  3. Is there a better way altogether, allowing me to verify the signature without re-implementing the JSON parsing?
Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74

2 Answers2

0

How do I raise a ParseError from a parser without a source exception?

You are expected to create the Plug.Parsers.ParseError yourself:

raise %Plug.Parsers.ParseError{exception: %MyException{message: "Failed to parse"}}

Do I need to raise the ParseError, or it it better to raise my own exception?

You might raise whatever you want, but since it’s indeed ParseError I do not see any reason to raise something different.

Is there a better way altogether [...]

This one is too opinionated. JSON parsing seems to be OK.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • Attempting to raise the error like that causes `** (FunctionClauseError) no function clause matching in Plug.Parsers.ParseError.exception/1` because ParserError expects to be passed an `exception` argument that is a struct. – Adam Millerchip Jul 04 '18 at 04:06
  • Oh sorry, I somehow screwed up a part of the code; There is no such thing as “exception instance” in Elixir due to obvious reasons; It actually accepts [any struct having `message` field](https://github.com/elixir-plug/plug/blob/v1.6.0/lib/plug/parsers.ex#L38). – Aleksei Matiushkin Jul 04 '18 at 04:09
  • Yeah that's the reason for my question. I have to create a struct that looks like an "exception" just to raise `ParseError`. In that case I might as well just raise my own Exception. Seems like `ParseError` wasn't written with this usage in mind. – Adam Millerchip Jul 04 '18 at 04:19
  • It is a generic wrapper for the `Plug` internals to pattern match it and handle it in a common way. It surely was written with this particular usage in mind. – Aleksei Matiushkin Jul 04 '18 at 04:20
  • In the two cases I can find in the Plug repository itself when `ParseError` is raised, it's is wrapping a rescued exception, not constructing a `ParseError` from unraised error cases. – Adam Millerchip Jul 04 '18 at 04:36
  • That is true; the custom code in e.g. my own projects on top of phoenix uses a single specific handler for all types of _parsing errors_. After all, this exception is dedicated to parsing errors and yours seems to be a parsing error, so I don’t see any reason to reinvent a wheel and raise something else. – Aleksei Matiushkin Jul 04 '18 at 04:51
  • 1
    Yeah. I decided to stick with `ParseError`, but to feed it the error generated from `Poison.decode!/2` after all. Seems the cleanest way. – Adam Millerchip Jul 04 '18 at 05:00
0

(I'd prefer to let Plug.Parsers.JSON do that, but I need to read the raw request body to verify it against a signature, so that's not possible).

...

  1. Is there a better way altogether, allowing me to verify the signature without re-implementing the JSON parsing?

Letting Plug.Parsers.JSON do that is in fact the best option. Since version 1.5.1 it's possible to provide a custom body reader to the parser that can cache the body for later use. This is a much more generic solution than re-implementing a JSON-parser plug.

Here is my custom reader:

def read_body(conn, opts) do
  case Plug.Conn.read_body(conn, opts) do
    {res, body, conn} when res in [:ok, :more] ->
      {res, body, update_in(conn.assigns[:raw_body], &((&1 || "") <> body))}

    unknown ->
      unknown
  end
end
Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74