Versions:
- newrelic_rpm 9.23.0 (also present in 10.2.0 — code is identical)
- httpx 1.7.5
- Ruby 3.4.4
- Passenger (multi-threaded)
Description:
nr_start_segment in lib/new_relic/agent/instrumentation/httpx/instrumentation.rb calls start_external_request_segment which can return nil when the internal rescue => exception catches an error. The return value is then used on line 26 without a nil check:
def nr_start_segment(request)
return unless NewRelic::Agent::Tracer.state.is_execution_traced?
wrapped_request = NewRelic::Agent::HTTPClients::HTTPXHTTPRequest.new(request)
segment = NewRelic::Agent::Tracer.start_external_request_segment(
library: wrapped_request.type,
uri: wrapped_request.uri,
procedure: wrapped_request.method
)
segment.add_request_headers(wrapped_request) # <-- crashes when segment is nil
# ...
end
start_external_request_segment in tracer.rb has:
def start_external_request_segment(library:, uri:, procedure:, start_time: nil, parent: nil)
segment = Transaction::ExternalRequestSegment.new(library, uri, procedure, start_time)
start_and_add_segment(segment, parent)
rescue ArgumentError
raise
rescue => exception
log_error('start_external_request_segment', exception)
# implicitly returns nil
end
When any exception occurs during segment creation (e.g. URI parsing edge case, threading issue), the rescue swallows it, logs it, and returns nil. The HTTPX instrumentation doesn't guard against this.
Error:
NoMethodError: undefined method 'add_request_headers' for nil
newrelic_rpm-9.23.0/lib/new_relic/agent/instrumentation/httpx/instrumentation.rb:26:in 'nr_start_segment'
newrelic_rpm-9.23.0/lib/new_relic/agent/instrumentation/httpx/instrumentation.rb:13:in 'block in send_requests_with_tracing'
newrelic_rpm-9.23.0/lib/new_relic/agent/instrumentation/httpx/instrumentation.rb:13:in 'Array#each'
newrelic_rpm-9.23.0/lib/new_relic/agent/instrumentation/httpx/instrumentation.rb:13:in 'send_requests_with_tracing'
newrelic_rpm-9.23.0/lib/new_relic/agent/instrumentation/httpx/prepend.rb:11:in 'send_requests'
Context: This occurs in a multi-threaded Passenger environment making concurrent HTTPX requests. The actual HTTP requests succeed (fired pixels are stored), but New Relic's instrumentation crashes during tracing setup.
Suggested fix:
Add a nil guard in nr_start_segment:
def nr_start_segment(request)
return unless NewRelic::Agent::Tracer.state.is_execution_traced?
wrapped_request = NewRelic::Agent::HTTPClients::HTTPXHTTPRequest.new(request)
segment = NewRelic::Agent::Tracer.start_external_request_segment(
library: wrapped_request.type,
uri: wrapped_request.uri,
procedure: wrapped_request.method
)
return unless segment # <-- guard against nil
segment.add_request_headers(wrapped_request)
request.on(:response) { nr_finish_segment.call(request, segment) }
end
Workaround: Disable HTTPX instrumentation in newrelic.yml:
instrumentation:
httpx: disabled
Versions:
Description:
nr_start_segmentinlib/new_relic/agent/instrumentation/httpx/instrumentation.rbcallsstart_external_request_segmentwhich can returnnilwhen the internalrescue => exceptioncatches an error. The return value is then used on line 26 without a nil check:start_external_request_segmentintracer.rbhas:When any exception occurs during segment creation (e.g. URI parsing edge case, threading issue), the rescue swallows it, logs it, and returns nil. The HTTPX instrumentation doesn't guard against this.
Error:
Context: This occurs in a multi-threaded Passenger environment making concurrent HTTPX requests. The actual HTTP requests succeed (fired pixels are stored), but New Relic's instrumentation crashes during tracing setup.
Suggested fix:
Add a nil guard in
nr_start_segment:Workaround: Disable HTTPX instrumentation in
newrelic.yml: