Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix username logging on 500 responses. #754

Merged
merged 7 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ Write the date in place of the "Unreleased" in the case a new version is release

# Changelog

## Unreleased

- Fix regression introduced in the previous release (v0.1.0b1) where exceptions
raised in the server sent _no_ response instead of properly sending a 500
response. (This presents in the client as, "Server disconnected without
sending a response.") A test now protects against this class of regression.

## v0.1.0b2 (2024-05-28)

## Changed
Expand Down
69 changes: 69 additions & 0 deletions tiled/_tests/test_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import contextlib
import threading
import time

import uvicorn
from fastapi import APIRouter
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR

from ..catalog import in_memory
from ..client import from_uri
from ..server.app import build_app
from ..server.logging_config import LOGGING_CONFIG

router = APIRouter()


class Server(uvicorn.Server):
# https://github.com/encode/uvicorn/discussions/1103#discussioncomment-941726

def install_signal_handlers(self):
pass

@contextlib.contextmanager
def run_in_thread(self):
thread = threading.Thread(target=self.run)
thread.start()
try:
# Wait for server to start up, or raise TimeoutError.
for _ in range(100):
time.sleep(0.1)
if self.started:
break
else:
raise TimeoutError("Server did not start in 10 seconds.")
host, port = self.servers[0].sockets[0].getsockname()
yield f"http://{host}:{port}"
finally:
self.should_exit = True
thread.join()


@router.get("/error")
def error():
1 / 0 # error!


def test_500_response():
"""
Test that unexpected server error returns 500 response.

This test is meant to catch regressions in which server exceptions can
result in the server sending no response at all, leading clients to raise
like:

httpx.RemoteProtocolError: Server disconnected without sending a response.

This can happen when bugs are introduced in the middleware layer.
"""
API_KEY = "secret"
catalog = in_memory()
app = build_app(catalog, {"single_user_api_key": API_KEY})
app.include_router(router)
config = uvicorn.Config(app, port=0, loop="asyncio", log_config=LOGGING_CONFIG)
server = Server(config)

with server.run_in_thread() as url:
client = from_uri(url, api_key=API_KEY)
response = client.context.http_client.get(f"{url}/error")
assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
4 changes: 4 additions & 0 deletions tiled/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ async def unsupported_query_type_exception_handler(
async def unhandled_exception_handler(
request: Request, exc: Exception
) -> JSONResponse:
# The current_principal_logging_filter middleware will not have
# had a chance to finish running, so set the principal here.
principal = getattr(request.state, "principal", None)
current_principal.set(principal)
return await http_exception_handler(
request,
HTTPException(
Expand Down
9 changes: 7 additions & 2 deletions tiled/server/principal_log_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ class PrincipalFilter(Filter):
"""Logging filter to attach username or Service Principal UUID to LogRecord"""

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