elixirplug

With block in Plug server throws MatchError instead of using else block


Previously, I would use with blocks in Elixir Plug servers to parse parameters from requests and return a sane response if that failed. However, that doesn't seem to be working anymore (Elixir 1.11). Can anyone point out what's going on?

Here is a minimalist Plug server that will show the problem

defmodule MatchTest.Router do
  use Plug.Router

  plug(:match)
  plug(:dispatch)

  get "/" do
    other_with =
      with {:ok, _} <- Map.fetch(%{}, "test") do
        :ok
      else
        :error -> :error
      end

    with conn <- Plug.Conn.fetch_query_params(conn),
         {:ok, a} = Map.fetch(conn.query_params, "a") do
      Plug.Conn.send_resp(conn, 200, "a = #{a}; other_with = #{other_with}")
    else
      :error -> Plug.Conn.send_resp(conn, 400, "Incorrect Parameters")
    end
  end

  match _ do
    Plug.Conn.send_resp(conn, 404, "Not Found")
  end
end

defmodule MatchTest do
  use Application

  def start(_type, _args) do
    Supervisor.start_link(
      [{Plug.Cowboy, scheme: :http, plug: MatchTest.Router, options: [port: 4000]}],
      strategy: :one_for_one,
      name: RateLimitedServer.Supervisor
    )
  end
end

As expected, when I include the a parameter in a GET request everything works:

ddrexler@Drexbook-Pro:temp|$ http -v get localhost:4000/ a=="test"
GET /?a=test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:4000
User-Agent: HTTPie/2.3.0



HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 28
date: Mon, 15 Feb 2021 22:40:14 GMT
server: Cowboy

a = test; other_with = error

In particular, the first with clause works as expected and, when we try to Map.fetch() from an empty map, it jumps over into the else clause (You can see this because the string "other_with = error").

However, when I try to exclude the parameter I get a 500:

ddrexler@Drexbook-Pro:~|$ http -v get localhost:4000/
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:4000
User-Agent: HTTPie/2.3.0



HTTP/1.1 500 Internal Server Error
content-length: 0

And the server gets an uncaught MatchError:

ddrexler@Drexbook-Pro:match_test|$ mix run --no-halt
Compiling 1 file (.ex)
warning: "else" clauses will never match because all patterns in "with" will always match
  lib/match_test.ex:15


14:40:16.341 [error] #PID<0.350.0> running MatchTest.Router (connection #PID<0.349.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /
** (exit) an exception was raised:
    ** (MatchError) no match of right hand side value: :error
        (match_test 0.1.0) lib/match_test.ex:16: anonymous fn/2 in MatchTest.Router.do_match/4
        (match_test 0.1.0) lib/plug/router.ex:284: MatchTest.Router.dispatch/2
        (match_test 0.1.0) lib/match_test.ex:1: MatchTest.Router.plug_builder_call/2
        (plug_cowboy 2.4.1) lib/plug/cowboy/handler.ex:12: Plug.Cowboy.Handler.init/2
        (cowboy 2.8.0) /Users/ddrexler/src/elixir/match_test/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.8.0) /Users/ddrexler/src/elixir/match_test/deps/cowboy/src/cowboy_stream_h.erl:300: :cowboy_stream_h.execute/3
        (cowboy 2.8.0) /Users/ddrexler/src/elixir/match_test/deps/cowboy/src/cowboy_stream_h.erl:291: :cowboy_stream_h.request_process/3
        (stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

Also note the warning "else" clauses will never match because all patterns in "with" will always match - is clearly untrue! There's a situation where they don't match because I get a MatchError. The else block includes an :error option, which should catch this result!


Solution

  • You've probably already worked it out from @sbacarob's comment.

    Your error is coming because when there's no a param: {:ok, a} = Map.fetch(conn.query_params, "a") is evaluated as {:ok, a} = :error and that raises the MatchError.

    To avoid the MatchError you need to use the special with-specific operator <-. You can think of it kind of like a "soft match" operator (where = is a "hard match"). The soft match lets the match fail (in a with) and fall through to the else/end, whereas the hard match will raise MatchError.