diff --git a/lib/sentry/transport.ex b/lib/sentry/transport.ex index f7f10440..36ae4fb0 100644 --- a/lib/sentry/transport.ex +++ b/lib/sentry/transport.ex @@ -136,7 +136,7 @@ defmodule Sentry.Transport do end defp update_rate_limits(headers, status) do - rate_limits_header = :proplists.get_value("X-Sentry-Rate-Limits", headers, nil) + rate_limits_header = get_header(headers, "x-sentry-rate-limits") cond do is_binary(rate_limits_header) -> @@ -154,7 +154,7 @@ defmodule Sentry.Transport do end defp get_global_delay(headers) do - with timeout when is_binary(timeout) <- :proplists.get_value("Retry-After", headers, nil), + with timeout when is_binary(timeout) <- get_header(headers, "retry-after"), {delay, ""} <- Integer.parse(timeout) do delay else @@ -164,6 +164,16 @@ defmodule Sentry.Transport do end end + defp get_header(headers, lowercase_name) do + Enum.find_value(headers, fn + {header_name, value} when is_binary(header_name) -> + if String.downcase(header_name) == lowercase_name, do: value + + _other -> + nil + end) + end + defp get_endpoint_and_headers do %Sentry.DSN{} = dsn = Config.dsn() diff --git a/test/sentry/telemetry_processor_integration_test.exs b/test/sentry/telemetry_processor_integration_test.exs index 6cab2394..61be3b53 100644 --- a/test/sentry/telemetry_processor_integration_test.exs +++ b/test/sentry/telemetry_processor_integration_test.exs @@ -311,9 +311,7 @@ defmodule Sentry.TelemetryProcessorIntegrationTest do ref = make_ref() request_count = :counters.new(1, []) - # Use HackneyClient because FinchClient (Mint) lowercases response headers, - # which prevents the transport from matching "X-Sentry-Rate-Limits". - put_test_config(client: Sentry.HackneyClient) + put_test_config(client: Sentry.FinchClient) Bypass.expect(ctx.bypass, "POST", "/api/1/envelope/", fn conn -> count = :counters.get(request_count, 1) diff --git a/test/sentry_test.exs b/test/sentry_test.exs index 964ad8bf..7ea4ea43 100644 --- a/test/sentry_test.exs +++ b/test/sentry_test.exs @@ -135,6 +135,32 @@ defmodule SentryTest do assert :ignored = Sentry.send_event(event) end + test "reads Retry-After response headers case-insensitively", %{bypass: bypass} do + request_count = :counters.new(1, []) + put_test_config(client: Sentry.FinchClient) + + Bypass.expect(bypass, "POST", "/api/1/envelope/", fn conn -> + request_number = :counters.get(request_count, 1) + :counters.add(request_count, 1, 1) + + if request_number == 0 do + conn + |> Plug.Conn.put_resp_header("Retry-After", "0") + |> Plug.Conn.resp(429, ~s<{}>) + else + Plug.Conn.resp(conn, 200, ~s<{"id": "#{Sentry.UUID.uuid4_hex()}"}>) + end + end) + + assert {:error, %Sentry.ClientError{reason: :rate_limited}} = + Sentry.capture_message("rate-limited", result: :sync, request_retries: []) + + assert {:ok, _event_id} = + Sentry.capture_message("accepted", result: :sync, request_retries: []) + + assert :counters.get(request_count, 1) == 2 + end + describe "send_check_in/1" do test "posts a check-in with all the explicit arguments", %{bypass: bypass} do put_test_config(environment_name: "test", release: "1.3.2") @@ -248,6 +274,47 @@ defmodule SentryTest do assert_sentry_report(:transaction, transaction: "test-transaction") end + test "does not apply categorized error rate limits to transactions", %{ + bypass: bypass, + transaction: transaction + } do + test_pid = self() + ref = make_ref() + request_count = :counters.new(1, []) + put_test_config(client: Sentry.FinchClient) + + Bypass.expect(bypass, "POST", "/api/1/envelope/", fn conn -> + request_number = :counters.get(request_count, 1) + :counters.add(request_count, 1, 1) + + if request_number == 0 do + conn + |> Plug.Conn.put_resp_header("X-Sentry-Rate-Limits", "60:error:organization") + |> Plug.Conn.resp(429, ~s<{"error": "Rate limited"}>) + else + {:ok, body, conn} = Plug.Conn.read_body(conn) + + if body =~ ~s("type":"transaction") do + send(test_pid, {:bypass_envelope, ref, body}) + end + + Plug.Conn.resp(conn, 200, ~s<{"id": "#{Sentry.UUID.uuid4_hex()}"}>) + end + end) + + assert {:error, %Sentry.ClientError{reason: :rate_limited}} = + Sentry.capture_message( + "rate-limited-error", + result: :sync, + request_retries: [] + ) + + assert {:ok, _event_id} = + Sentry.send_transaction(transaction, result: :sync, request_retries: []) + + assert_sentry_transaction(ref, transaction: "test-transaction") + end + test "validates options", %{transaction: transaction} do assert_raise NimbleOptions.ValidationError, fn -> Sentry.send_transaction(transaction, client: "oops")