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