Skip to content

Commit a5da3ce

Browse files
authored
fix: throwing plug properly handled and returns 500 (#411)
* fix: throwing plug returns 500 * Keeps backwards compatibility for telemetry span exception event * mix format
1 parent 3e12dd4 commit a5da3ce

File tree

4 files changed

+87
-19
lines changed

4 files changed

+87
-19
lines changed

lib/bandit/pipeline.ex

+20-13
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ defmodule Bandit.Pipeline do
4646
{:upgrade, adapter.transport, protocol, opts}
4747
end
4848
rescue
49-
error -> handle_error(error, __STACKTRACE__, transport, span, opts)
49+
exception -> handle_error(:error, exception, __STACKTRACE__, transport, span, opts)
50+
catch
51+
:throw, value ->
52+
handle_error(:throw, value, __STACKTRACE__, transport, span, opts)
5053
end
5154
rescue
52-
error ->
55+
exception ->
5356
span = Bandit.Telemetry.start_span(:request, measurements, metadata)
54-
handle_error(error, __STACKTRACE__, transport, span, opts)
57+
handle_error(:error, exception, __STACKTRACE__, transport, span, opts)
5558
end
5659
end
5760

@@ -194,13 +197,14 @@ defmodule Bandit.Pipeline do
194197
end
195198

196199
@spec handle_error(
197-
Exception.t(),
200+
:error | :throw,
201+
Exception.t() | term(),
198202
Exception.stacktrace(),
199203
Bandit.HTTPTransport.t(),
200204
Bandit.Telemetry.t(),
201205
map()
202206
) :: {:ok, Bandit.HTTPTransport.t()} | {:error, term()}
203-
defp handle_error(%type{} = error, stacktrace, transport, span, opts)
207+
defp handle_error(:error, %type{} = error, stacktrace, transport, span, opts)
204208
when type in [
205209
Bandit.HTTPError,
206210
Bandit.HTTP2.Errors.StreamError,
@@ -216,21 +220,21 @@ defmodule Bandit.Pipeline do
216220
{:error, error}
217221
end
218222

219-
defp handle_error(error, stacktrace, transport, span, opts) do
220-
Bandit.Telemetry.span_exception(span, :exit, error, stacktrace)
221-
status = error |> Plug.Exception.status() |> Plug.Conn.Status.code()
223+
defp handle_error(kind, reason, stacktrace, transport, span, opts) do
224+
Bandit.Telemetry.span_exception(span, kind, reason, stacktrace)
225+
status = reason |> Plug.Exception.status() |> Plug.Conn.Status.code()
222226

223227
if status in Keyword.get(opts.http, :log_exceptions_with_status_codes, 500..599) do
224228
Logger.error(
225-
Exception.format(:error, error, stacktrace),
229+
Exception.format(kind, reason, stacktrace),
226230
domain: [:bandit],
227-
crash_reason: {error, stacktrace}
231+
crash_reason: crash_reason(kind, reason, stacktrace)
228232
)
229233

230-
Bandit.HTTPTransport.send_on_error(transport, error)
231-
{:error, error}
234+
Bandit.HTTPTransport.send_on_error(transport, reason)
235+
{:error, reason}
232236
else
233-
Bandit.HTTPTransport.send_on_error(transport, error)
237+
Bandit.HTTPTransport.send_on_error(transport, reason)
234238
{:ok, transport}
235239
end
236240
end
@@ -252,4 +256,7 @@ defmodule Bandit.Pipeline do
252256
end
253257

254258
defp do_maybe_log_error(_error, _stacktrace, false), do: :ok
259+
260+
defp crash_reason(:throw, reason, stacktrace), do: {{:nocatch, reason}, stacktrace}
261+
defp crash_reason(_, reason, stacktrace), do: {reason, stacktrace}
255262
end

lib/bandit/telemetry.ex

+10-4
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,24 @@ defmodule Bandit.Telemetry do
188188
untimed_span_event(span, :stop, measurements, metadata)
189189
end
190190

191-
@spec span_exception(t(), Exception.kind(), Exception.t(), Exception.stacktrace()) :: :ok
192-
def span_exception(span, kind, exception, stacktrace) do
191+
@spec span_exception(t(), Exception.kind(), Exception.t() | term(), Exception.stacktrace()) ::
192+
:ok
193+
def span_exception(span, :error, reason, stacktrace) when is_exception(reason) do
193194
metadata =
194195
Map.merge(span.start_metadata, %{
195-
kind: kind,
196-
exception: exception,
196+
# Using :exit for backwards-compatiblity with Bandit =< 1.5.7
197+
kind: :exit,
198+
exception: reason,
197199
stacktrace: stacktrace
198200
})
199201

200202
span_event(span, :exception, %{}, metadata)
201203
end
202204

205+
def span_exception(_span, _kind, _reason, _stacktrace) do
206+
:ok
207+
end
208+
203209
@doc false
204210
@spec span_event(t(), span_name(), :telemetry.event_measurements(), :telemetry.event_metadata()) ::
205211
:ok

test/bandit/http1/request_test.exs

+36-1
Original file line numberDiff line numberDiff line change
@@ -1793,6 +1793,21 @@ defmodule HTTP1RequestTest do
17931793
raise "boom"
17941794
end
17951795

1796+
test "returns a 500 if the plug throws", context do
1797+
output =
1798+
capture_log(fn ->
1799+
response = Req.get!(context.req, url: "/throws")
1800+
assert response.status == 500
1801+
Process.sleep(100)
1802+
end)
1803+
1804+
assert output =~ "(throw) \"something\""
1805+
end
1806+
1807+
def throws(_conn) do
1808+
throw("something")
1809+
end
1810+
17961811
test "does not send an error response if the plug has already sent one before raising",
17971812
context do
17981813
output =
@@ -2176,7 +2191,7 @@ defmodule HTTP1RequestTest do
21762191
assert output =~ "(Bandit.HTTPError) Header read timeout"
21772192
end
21782193

2179-
test "it should send `exception` events for erroring requests", context do
2194+
test "it should send `exception` events for raising requests", context do
21802195
output =
21812196
capture_log(fn ->
21822197
{:ok, collector_pid} =
@@ -2202,6 +2217,26 @@ defmodule HTTP1RequestTest do
22022217

22032218
assert output =~ "(RuntimeError) boom"
22042219
end
2220+
2221+
test "it should not send `exception` events for throwing requests", context do
2222+
output =
2223+
capture_log(fn ->
2224+
{:ok, collector_pid} =
2225+
start_supervised({Bandit.TelemetryCollector, [[:bandit, :request, :exception]]})
2226+
2227+
Req.get!(context.req, url: "/uncaught_throw")
2228+
2229+
Process.sleep(100)
2230+
2231+
assert [] = Bandit.TelemetryCollector.get_events(collector_pid)
2232+
end)
2233+
2234+
assert output =~ "(throw) \"thrown\""
2235+
end
2236+
2237+
def uncaught_throw(_conn) do
2238+
throw("thrown")
2239+
end
22052240
end
22062241

22072242
describe "connection closure / error handling" do

test/bandit/http2/plug_test.exs

+21-1
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ defmodule HTTP2PlugTest do
996996
assert output =~ "(Bandit.HTTP2.Errors.StreamError) Received uppercase header"
997997
end
998998

999-
test "it should send `exception` events for erroring requests", context do
999+
test "it should send `exception` events for raising requests", context do
10001000
output =
10011001
capture_log(fn ->
10021002
{:ok, collector_pid} =
@@ -1026,5 +1026,25 @@ defmodule HTTP2PlugTest do
10261026
def raise_error(_conn) do
10271027
raise "boom"
10281028
end
1029+
1030+
test "it should not send `exception` events for throwing requests", context do
1031+
output =
1032+
capture_log(fn ->
1033+
{:ok, collector_pid} =
1034+
start_supervised({Bandit.TelemetryCollector, [[:bandit, :request, :exception]]})
1035+
1036+
Req.get!(context.req, url: "/uncaught_throw")
1037+
1038+
Process.sleep(100)
1039+
1040+
assert [] = Bandit.TelemetryCollector.get_events(collector_pid)
1041+
end)
1042+
1043+
assert output =~ "(throw) \"thrown\""
1044+
end
1045+
1046+
def uncaught_throw(_conn) do
1047+
throw("thrown")
1048+
end
10291049
end
10301050
end

0 commit comments

Comments
 (0)