Skip to content

Commit 4591d64

Browse files
mitchh456claude
andauthored
Verify 401 responses are not tagged as errors (fixes #838) (#849)
* Use Job.id property instead of removed get_id() method rq v2.7 refactored Job.id from `id = property(get_id, set_id)` to a `@property` decorator, removing get_id() as a callable method. Job.id has been available since rq v0.5.0 so this is backwards compatible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Support FastMCP 3.x while maintaining 2.x backwards compatibility FastMCP 3.0 replaced private _call_tool_mcp()/_list_tools_mcp() with public call_tool()/list_tools() methods, and changed get_tool() to return None instead of raising when a tool is not found. - Update instrumentation to handle get_tool() returning None - Update tests with compat helpers that use the correct API based on the installed fastmcp version - Remove the fastmcp<3 version pin from tox.ini (no longer needed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add test verifying 401 responses are not tagged as errors Adds a test endpoint returning 401 and a corresponding test that confirms Scout's Starlette/FastAPI middleware correctly tracks 401 Unauthorized responses without tagging them as errors. Only 5xx responses trigger the error tag, so a FastAPI OAuth2 empty bearer token rejection (which returns 401) is properly categorized as a client error, not a server error. Closes #838 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 19081a5 commit 4591d64

1 file changed

Lines changed: 32 additions & 0 deletions

File tree

tests/integration/test_starlette.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ async def crash(request):
5858
async def return_error(request):
5959
return PlainTextResponse("Something went wrong", status_code=503)
6060

61+
async def return_unauthorized(request):
62+
return PlainTextResponse("Unauthorized", status_code=401)
63+
6164
async def background_jobs(request):
6265
def sync_noop():
6366
pass
@@ -85,6 +88,7 @@ async def __call__(self, scope, receive, send):
8588
Route("/sync-hello/", endpoint=SyncHelloEndpoint),
8689
Route("/crash/", endpoint=crash),
8790
Route("/return-error/", endpoint=return_error),
91+
Route("/return-unauthorized/", endpoint=return_unauthorized),
8892
Route("/background-jobs/", endpoint=background_jobs),
8993
Route("/instance-app/", endpoint=InstanceApp()),
9094
]
@@ -474,3 +478,31 @@ async def test_instance_app(tracked_requests):
474478
"Controller/tests.integration.test_starlette."
475479
+ "app_with_scout.<locals>.InstanceApp"
476480
)
481+
482+
483+
@pytest.mark.asyncio
484+
async def test_return_unauthorized_not_tagged_as_error(tracked_requests):
485+
"""
486+
Verify that a 401 Unauthorized response is tracked but NOT tagged as an
487+
error. Only 5xx responses should be tagged as errors. This is the correct
488+
behavior when, for example, a FastAPI OAuth2 dependency rejects an empty
489+
bearer token: the 401 is a normal client error, not a server error.
490+
491+
See: https://github.com/scoutapp/scout_apm_python/issues/838
492+
"""
493+
with app_with_scout() as app:
494+
communicator = ApplicationCommunicator(
495+
app, asgi_http_scope(path="/return-unauthorized/")
496+
)
497+
await communicator.send_input({"type": "http.request"})
498+
response_start = await communicator.receive_output()
499+
await communicator.receive_output()
500+
501+
assert response_start["type"] == "http.response.start"
502+
assert response_start["status"] == 401
503+
assert len(tracked_requests) == 1
504+
tracked_request = tracked_requests[0]
505+
assert len(tracked_request.complete_spans) == 1
506+
assert tracked_request.tags["path"] == "/return-unauthorized/"
507+
# 401 must NOT be tagged as an error — only 5xx responses are errors
508+
assert "error" not in tracked_request.tags

0 commit comments

Comments
 (0)