Skip to content

Commit 38e2f09

Browse files
committed
Fix crash when asgi wrapper is used more than once.
1 parent f7ce482 commit 38e2f09

File tree

3 files changed

+90
-5
lines changed

3 files changed

+90
-5
lines changed

newrelic/api/asgi_application.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616

1717
import newrelic.packages.asgiref_compatibility as asgiref_compatibility
1818
import newrelic.packages.six as six
19+
from newrelic.api.transaction import current_transaction
1920
from newrelic.api.application import application_instance
2021
from newrelic.api.web_transaction import WebTransaction
2122
from newrelic.common.object_names import callable_name
22-
from newrelic.common.object_wrapper import wrap_object, FunctionWrapper, function_wrapper
23+
from newrelic.common.object_wrapper import (
24+
wrap_object,
25+
FunctionWrapper,
26+
function_wrapper,
27+
)
2328
from newrelic.common.async_proxy import CoroutineProxy, LoopContext
2429
from newrelic.api.html_insertion import insert_html_snippet, verify_body_exists
2530

@@ -105,7 +110,7 @@ def should_insert_html(self, headers):
105110
# value 'identity' is used. Technically the value
106111
# 'identity' should only be used in the header
107112
# Accept-Encoding and not Content-Encoding. In
108-
# other words, a WSGI application should not be
113+
# other words, a ASGI application should not be
109114
# returning identity. We could check and allow it
110115
# anyway and still do RUM insertion, but don't.
111116

@@ -251,21 +256,65 @@ async def send(self, event):
251256
def ASGIApplicationWrapper(
252257
wrapped, application=None, name=None, group=None, framework=None
253258
):
254-
255259
def nr_asgi_wrapper(wrapped, instance, args, kwargs):
256260
double_callable = asgiref_compatibility.is_double_callable(wrapped)
257261
if double_callable:
258262
is_v2_signature = (len(args) + len(kwargs)) == 1
259263
if not is_v2_signature:
260264
return wrapped(*args, **kwargs)
261-
wrapped = double_to_single_callable(wrapped)
262265

263266
scope = _bind_scope(*args, **kwargs)
264267

265268
if scope["type"] != "http":
266269
return wrapped(*args, **kwargs)
267270

268271
async def nr_async_asgi(receive, send):
272+
# Check to see if any transaction is present, even an inactive
273+
# one which has been marked to be ignored or which has been
274+
# stopped already.
275+
276+
transaction = current_transaction(active_only=False)
277+
278+
if transaction:
279+
# If there is any active transaction we will return without
280+
# applying a new ASGI application wrapper context. In the
281+
# case of a transaction which is being ignored or which has
282+
# been stopped, we do that without doing anything further.
283+
284+
if transaction.ignore_transaction or transaction.stopped:
285+
return await wrapped(scope, receive, send)
286+
287+
# For any other transaction, we record the details of any
288+
# framework against the transaction for later reporting as
289+
# supportability metrics.
290+
291+
if framework:
292+
transaction.add_framework_info(
293+
name=framework[0], version=framework[1]
294+
)
295+
296+
# Also override the web transaction name to be the name of
297+
# the wrapped callable if not explicitly named, and we want
298+
# the default name to be that of the ASGI component for the
299+
# framework. This will override the use of a raw URL which
300+
# can result in metric grouping issues where a framework is
301+
# not instrumented or is leaking URLs.
302+
303+
settings = transaction._settings
304+
305+
if name is None and settings:
306+
if framework is not None:
307+
naming_scheme = settings.transaction_name.naming_scheme
308+
if naming_scheme in (None, "framework"):
309+
transaction.set_transaction_name(
310+
callable_name(wrapped), priority=1
311+
)
312+
313+
elif name:
314+
transaction.set_transaction_name(name, group, priority=1)
315+
316+
return await wrapped(scope, receive, send)
317+
269318
with ASGIWebTransaction(
270319
application=application_instance(application),
271320
scope=scope,
@@ -324,6 +373,7 @@ async def nr_async_asgi(receive, send):
324373
return await coro
325374

326375
if double_callable:
376+
wrapped = double_to_single_callable(wrapped)
327377
return nr_async_asgi
328378
else:
329379
return nr_async_asgi(*_bind_receive_send(*args, **kwargs))

tests/agent_features/test_asgi_transaction.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import asyncio
16+
import logging
1517
import pytest
1618
from testing_support.sample_asgi_applications import simple_app_v2_raw, simple_app_v3_raw, simple_app_v2, simple_app_v3, AppWithDescriptor
1719
from testing_support.fixtures import validate_transaction_metrics, override_application_settings, function_not_called
@@ -124,3 +126,34 @@ def test_app_with_descriptor(method):
124126
assert response.status == 200
125127
assert response.headers == {}
126128
assert response.body == b""
129+
130+
131+
# Verify that errors are not generated when using multiple wrappers
132+
def test_multiple_calls_to_asgi_wrapper(caplog):
133+
app = ASGIApplicationWrapper(simple_app_v3)
134+
app = AsgiTest(app)
135+
136+
with caplog.at_level(logging.ERROR, logger="newrelic"):
137+
response = app.make_request("GET", "/")
138+
139+
assert response.status == 200
140+
assert not caplog.records
141+
142+
143+
def test_non_http_scope_v3():
144+
async def _test():
145+
await simple_app_v3({"type": "cookies"}, None, None)
146+
147+
loop = asyncio.get_event_loop()
148+
with pytest.raises(ValueError):
149+
loop.run_until_complete(_test())
150+
151+
152+
def test_non_http_scope_v2():
153+
async def _test():
154+
call_me = simple_app_v2({"type": "cookies"})
155+
await call_me(None, None)
156+
157+
loop = asyncio.get_event_loop()
158+
with pytest.raises(ValueError):
159+
loop.run_until_complete(_test())

tests/testing_support/sample_asgi_applications.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919

2020
class simple_app_v2_raw:
2121
def __init__(self, scope):
22-
assert scope["type"] == "http"
22+
self.scope = scope
2323

2424
async def __call__(self, receive, send):
25+
if self.scope["type"] != "http":
26+
raise ValueError("unsupported")
2527
await send({"type": "http.response.start", "status": 200})
2628
await send({"type": "http.response.body"})
2729

0 commit comments

Comments
 (0)