View Source Errors, escaping and halting
As a request is moving through a pipeline, some steps may produce errors. In that case the pipeline stops and returns the first error encountered, without calling the rest of the pipes.
This is not always the desired behavior. Let's consider a pipeline where the first few pipes validate different aspects of the request.
defmodule Example do
use Plumbery
import Plumbery.Request
defp validate_x(req = %{command: %{x: x}}) when is_number(x) and x > 0, do: req
defp validate_x(req), do: add_error(req, :x, "is invalid")
defp validate_y(req = %{command: %{y: y}}) when is_number(y) and y > 0, do: req
defp validate_y(req), do: add_error(req, :y, "is invalid")
defp validate_name(req = %{command: %{name: "John"}}), do: req
defp validate_name(req), do: add_error(req, :name, "not John")
pipeline :validate_number_y do
pipe :validate_y
end
defp calculate(req = %{command: command}) do
success(req, "Hello #{command.name}! x+y = #{command.x + command.y}")
end
pipeline :run do
pipe :validate_x
pipe :validate_number_y
pipe :validate_name
pipe :calculate
end
end
# We expect to see 3 errors, but this fails
%{result: {:error, [x: "is invalid", y: "is invalid", name: "not John"]}} =
Example.run(%Plumbery.Request{command: %{name: "Jim", x: -20, y: 0}})
# This is what we actually get
%{result: {:error, [x: "is invalid"]}} =
Example.run(%Plumbery.Request{command: %{name: "Jim", x: -20, y: 0}})
When calling the pipeline, we want it to report validation errors for all aspects, not just the one it happens to check first. That is where the concept of escaping comes into play. By default a pipeline escapes on error, meaning that as soon as the request has an error, the pipeline returns immediately without calling any further steps. After escaping is turned off, all subsequent pipes will be called regardless of wheter the request has errors or not. That provides an opportunity for counter-acting an error or adding another error that is orthogonal to the ones generated by previous steps.
Let's modify the example so it actually does what we want.
defmodule Example do
use Plumbery
import Plumbery.Request
defp validate_x(req = %{command: %{x: x}}) when is_number(x) and x > 0, do: req
defp validate_x(req), do: add_error(req, :x, "is invalid")
defp validate_y(req = %{command: %{y: y}}) when is_number(y) and y > 0, do: req
defp validate_y(req), do: add_error(req, :y, "is invalid")
defp validate_name(req = %{command: %{name: "John"}}), do: req
defp validate_name(req), do: add_error(req, :name, "not John")
pipeline :validate_number_y do
escape_on_error false
pipe :validate_y
end
defp calculate(req = %{command: command}) do
success(req, "Hello #{command.name}! x+y = #{command.x + command.y}")
end
pipeline :run do
escape_on_error false
pipe :validate_x
pipe :validate_number_y
pipe :validate_name
escape_on_error true
pipe :calculate
end
end
# Now all expected errors are reported
%{result: {:error, [x: "is invalid", y: "is invalid", name: "not John"]}} =
Example.run(%Plumbery.Request{command: %{name: "Jim", x: -20, y: 0}})
%{result: {:error, [x: "is invalid", y: "is invalid"]}} =
Example.run(%Plumbery.Request{command: %{name: "John", x: -20, y: 0}})
# And the `:calculate' pipe is called only if there were no errors
%{result: {:ok, "Hello John! x+y = 99"}} =
Example.run(%Plumbery.Request{command: %{name: "John", x: 90, y: 9}})
So how does escape_on_error
options work? That's pretty simple. If it is
true
(the default value) then as soon as there is an error in the request, the
pipeline returns immediately. When that option is false
, all subsequent pipes
will be called.
Plumbery provides one more way of controlling the pipeline – pipeline halting.
Any pipe can call Plumbery.Request.halt/1
. That causes the pipeline to return
immeditely and unconditionally.
defmodule Example do
use Plumbery
import Plumbery.Request
def halter(req) do
req
|> success(:halted)
|> halt()
end
def skipper(req) do
req
|> success(:skipped)
end
pipeline :haltable do
pipe :halter
pipe :skipper
end
end
# skipper will not have a chance to be called because halter halts the pipeline
%{result: {:ok, :halted}, halted?: true} =
Example.haltable(%Plumbery.Request{})