Skip to content

Commit 2e9fe8c

Browse files
dl-alexandreOpenCode
andcommitted
fix(live_view_hook): handle Bandit.TransportError and URI struct in connect_info
Wrap `get_connect_info/2` calls in `try/rescue` to gracefully handle `Bandit.TransportError` when the WebSocket transport is already closed during mount (fixes #1025). Convert `socket.host_uri` to a string with `URI.to_string/1` before storing it in request context, preventing JSON encoding failures when `:uri` is not included in `connect_info` (fixes #1040). Also sanitize non-JSON-encodable values in `:request` during event rendering as defense-in-depth. Co-Authored-By: OpenCode <noreply@opencode.ai>
1 parent 01846b9 commit 2e9fe8c

4 files changed

Lines changed: 95 additions & 4 deletions

File tree

lib/sentry/client.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,12 @@ defmodule Sentry.Client do
298298
|> update_if_present(:breadcrumbs, fn bcs -> Enum.map(bcs, &Map.from_struct/1) end)
299299
|> update_if_present(:sdk, &Map.from_struct/1)
300300
|> update_if_present(:message, &sanitize_message(&1, json_library))
301-
|> update_if_present(:request, &(&1 |> Map.from_struct() |> remove_nils()))
301+
|> update_if_present(:request, fn req ->
302+
req
303+
|> Map.from_struct()
304+
|> remove_nils()
305+
|> sanitize_non_jsonable_values(json_library)
306+
end)
302307
|> update_if_present(:extra, &sanitize_non_jsonable_values(&1, json_library))
303308
|> update_if_present(:user, &sanitize_non_jsonable_values(&1, json_library))
304309
|> update_if_present(:tags, &sanitize_non_jsonable_values(&1, json_library))

lib/sentry/live_view_hook.ex

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,10 @@ if Code.ensure_loaded?(Phoenix.LiveView) do
156156

157157
defp on_mount(params, %Phoenix.LiveView.Socket{} = socket) do
158158
Context.set_extra_context(%{socket_id: socket.id})
159-
Context.set_request_context(%{url: socket.host_uri})
159+
160+
if uri = socket.host_uri do
161+
Context.set_request_context(%{url: URI.to_string(uri)})
162+
end
160163

161164
Context.add_breadcrumb(%{
162165
category: "web.live_view.mount",
@@ -231,8 +234,15 @@ if Code.ensure_loaded?(Phoenix.LiveView) do
231234

232235
defp get_connect_info_if_root(socket, key) do
233236
case socket.parent_pid do
234-
nil -> get_connect_info(socket, key)
235-
pid when is_pid(pid) -> nil
237+
nil ->
238+
try do
239+
get_connect_info(socket, key)
240+
rescue
241+
_ -> nil
242+
end
243+
244+
pid when is_pid(pid) ->
245+
nil
236246
end
237247
end
238248

test/sentry/client_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ defmodule Sentry.ClientTest do
5252
}
5353
end
5454

55+
test "safely inspects non-JSON-encodable values in :request" do
56+
event =
57+
Event.create_event(request: %{url: %URI{scheme: "http", host: "example.com", port: 80}})
58+
59+
rendered = Client.render_event(event)
60+
assert rendered.request.url == inspect(%URI{scheme: "http", host: "example.com", port: 80})
61+
end
62+
5563
test "safely inspects terms that cannot be converted to JSON" do
5664
event =
5765
Event.create_event(

test/sentry/live_view_hook_test.exs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ defmodule SentryTest.Endpoint do
129129
plug SentryTest.Router
130130
end
131131

132+
defmodule SentryTest.EndpointNoUri do
133+
use Phoenix.Endpoint, otp_app: :sentry
134+
135+
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [:peer_data, :user_agent]]
136+
137+
plug SentryTest.Router
138+
end
139+
132140
defmodule Sentry.LiveViewHookTest do
133141
use Sentry.Case, async: false
134142

@@ -327,3 +335,63 @@ defmodule Sentry.LiveViewHookTest do
327335
sentry_context
328336
end
329337
end
338+
339+
defmodule Sentry.LiveViewHookNoUriTest do
340+
use Sentry.Case, async: false
341+
342+
import ExUnit.CaptureLog
343+
import Phoenix.ConnTest
344+
import Phoenix.LiveViewTest
345+
346+
@endpoint SentryTest.EndpointNoUri
347+
348+
setup_all do
349+
Application.put_env(:sentry, SentryTest.EndpointNoUri,
350+
secret_key_base: "TMnue44VMTf1VmyD6SYKR30cqKpHluHOFZGXcVkC33hKVVKTVZ1HBQLLLLLLLLLL",
351+
live_view: [signing_salt: "F8ftIAbYdeTzhwgl"]
352+
)
353+
354+
pid = start_supervised!(SentryTest.EndpointNoUri)
355+
Process.link(pid)
356+
:ok
357+
end
358+
359+
setup do
360+
%{conn: build_conn()}
361+
end
362+
363+
test "stores string URL when :uri is not in connect_info", %{conn: conn} do
364+
conn = Plug.Conn.put_req_header(conn, "user-agent", "sentry-testing 1.0")
365+
366+
{:ok, view, html} = live(conn, "/hook_test")
367+
assert html =~ "<h1>Testing Sentry hooks</h1>"
368+
369+
context = get_sentry_context(view)
370+
371+
assert is_binary(context.request.url)
372+
assert context.request.url == "http://www.example.com/hook_test"
373+
assert context.extra.user_agent == "sentry-testing 1.0"
374+
end
375+
376+
test "does not log a Sentry error when connect_info is unavailable", %{conn: conn} do
377+
conn = Plug.Conn.put_req_header(conn, "user-agent", "sentry-testing 1.0")
378+
379+
logs =
380+
capture_log(fn ->
381+
{:ok, _view, _html} = live(conn, "/hook_test")
382+
end)
383+
384+
refute logs =~ "Sentry.LiveView.on_mount hook errored out"
385+
end
386+
387+
defp get_sentry_context(view) do
388+
{:dictionary, pdict} = Process.info(view.pid, :dictionary)
389+
390+
assert {:ok, sentry_context} =
391+
pdict
392+
|> Keyword.fetch!(:"$logger_metadata$")
393+
|> Map.fetch(Sentry.Context.__logger_metadata_key__())
394+
395+
sentry_context
396+
end
397+
end

0 commit comments

Comments
 (0)