Skip to content

Commit 3c19b6d

Browse files
authored
Fix username logging on 500 responses. (#754)
* Fix username logging on 500 responses. A bug causes the server to send no response, such that the client raised: ``` RemoteProtocolError: Server disconnected without sending a response. ``` instead of returning 500 response. This was a regression introduced in #750. Needs test. Closes #750 * Add comment * Refine comments * TST: Reproduce 'Server disconnected without sending a response' in test. * Update CHANGELOG * Clean up prints in test * Refactor so server can be shut down.
1 parent 9059fd0 commit 3c19b6d

File tree

4 files changed

+87
-2
lines changed

4 files changed

+87
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ Write the date in place of the "Unreleased" in the case a new version is release
33

44
# Changelog
55

6+
## Unreleased
7+
8+
- Fix regression introduced in the previous release (v0.1.0b1) where exceptions
9+
raised in the server sent _no_ response instead of properly sending a 500
10+
response. (This presents in the client as, "Server disconnected without
11+
sending a response.") A test now protects against this class of regression.
12+
613
## v0.1.0b2 (2024-05-28)
714

815
## Changed

tiled/_tests/test_server.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import contextlib
2+
import threading
3+
import time
4+
5+
import uvicorn
6+
from fastapi import APIRouter
7+
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
8+
9+
from ..catalog import in_memory
10+
from ..client import from_uri
11+
from ..server.app import build_app
12+
from ..server.logging_config import LOGGING_CONFIG
13+
14+
router = APIRouter()
15+
16+
17+
class Server(uvicorn.Server):
18+
# https://github.com/encode/uvicorn/discussions/1103#discussioncomment-941726
19+
20+
def install_signal_handlers(self):
21+
pass
22+
23+
@contextlib.contextmanager
24+
def run_in_thread(self):
25+
thread = threading.Thread(target=self.run)
26+
thread.start()
27+
try:
28+
# Wait for server to start up, or raise TimeoutError.
29+
for _ in range(100):
30+
time.sleep(0.1)
31+
if self.started:
32+
break
33+
else:
34+
raise TimeoutError("Server did not start in 10 seconds.")
35+
host, port = self.servers[0].sockets[0].getsockname()
36+
yield f"http://{host}:{port}"
37+
finally:
38+
self.should_exit = True
39+
thread.join()
40+
41+
42+
@router.get("/error")
43+
def error():
44+
1 / 0 # error!
45+
46+
47+
def test_500_response():
48+
"""
49+
Test that unexpected server error returns 500 response.
50+
51+
This test is meant to catch regressions in which server exceptions can
52+
result in the server sending no response at all, leading clients to raise
53+
like:
54+
55+
httpx.RemoteProtocolError: Server disconnected without sending a response.
56+
57+
This can happen when bugs are introduced in the middleware layer.
58+
"""
59+
API_KEY = "secret"
60+
catalog = in_memory()
61+
app = build_app(catalog, {"single_user_api_key": API_KEY})
62+
app.include_router(router)
63+
config = uvicorn.Config(app, port=0, loop="asyncio", log_config=LOGGING_CONFIG)
64+
server = Server(config)
65+
66+
with server.run_in_thread() as url:
67+
client = from_uri(url, api_key=API_KEY)
68+
response = client.context.http_client.get(f"{url}/error")
69+
assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR

tiled/server/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ async def unsupported_query_type_exception_handler(
330330
async def unhandled_exception_handler(
331331
request: Request, exc: Exception
332332
) -> JSONResponse:
333+
# The current_principal_logging_filter middleware will not have
334+
# had a chance to finish running, so set the principal here.
335+
principal = getattr(request.state, "principal", None)
336+
current_principal.set(principal)
333337
return await http_exception_handler(
334338
request,
335339
HTTPException(

tiled/server/principal_log_filter.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ class PrincipalFilter(Filter):
88
"""Logging filter to attach username or Service Principal UUID to LogRecord"""
99

1010
def filter(self, record: LogRecord) -> bool:
11-
principal = current_principal.get()
12-
if isinstance(principal, SpecialUsers):
11+
principal = current_principal.get(None)
12+
if principal is None:
13+
# This will only occur if an uncaught exception was raised in the
14+
# server before the authentication code ran. This will always be
15+
# associated with a 500 Internal Server Error response.
16+
short_name = "unset"
17+
elif isinstance(principal, SpecialUsers):
1318
short_name = f"{principal.value}"
1419
elif principal.type == "service":
1520
short_name = f"service:{principal.uuid}"

0 commit comments

Comments
 (0)