From 19db26b0cb27815c8e61c5b51c2926df6012f780 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Oct 2024 08:20:37 -0500 Subject: [PATCH] better errors --- src/fastapi_dynamic_response/base/router.py | 24 +++- .../logging_config.py | 9 +- src/fastapi_dynamic_response/main.py | 13 ++- src/fastapi_dynamic_response/middleware.py | 109 +++++++----------- src/fastapi_dynamic_response/settings.py | 11 ++ 5 files changed, 84 insertions(+), 82 deletions(-) diff --git a/src/fastapi_dynamic_response/base/router.py b/src/fastapi_dynamic_response/base/router.py index 5112381..7e752d1 100644 --- a/src/fastapi_dynamic_response/base/router.py +++ b/src/fastapi_dynamic_response/base/router.py @@ -1,6 +1,4 @@ -from fastapi import APIRouter -from fastapi import Depends -from fastapi import Request +from fastapi import APIRouter, Depends, Request from fastapi_dynamic_response.base.schema import Message from fastapi_dynamic_response.dependencies import get_content_type @@ -17,6 +15,16 @@ async def get_example( return {"message": "Hello, this is an example", "data": [1, 2, 3, 4]} +@router.get("/error") +async def get_error( + request: Request, + content_type: str = Depends(get_content_type), +): + request.state.template_name = "example.html" + 0 / 0 + return {"message": "Hello, this is an example", "data": [1, 2, 3, 4]} + + @router.get("/another-example") async def another_example( request: Request, @@ -30,6 +38,16 @@ async def another_example( } +@router.get("/message") +async def message( + request: Request, + message_id: int, + content_type: str = Depends(get_content_type), +): + request.state.template_name = "post_message.html" + return {"message": message.message} + + @router.post("/message") async def message( request: Request, diff --git a/src/fastapi_dynamic_response/logging_config.py b/src/fastapi_dynamic_response/logging_config.py index b20efc5..80f0a3f 100644 --- a/src/fastapi_dynamic_response/logging_config.py +++ b/src/fastapi_dynamic_response/logging_config.py @@ -3,15 +3,13 @@ import logging -from fastapi_dynamic_response.settings import Settings +from fastapi_dynamic_response.settings import settings import structlog logger = structlog.get_logger() -def configure_logging_two(): - settings = Settings() - +def configure_logging(): # Clear existing loggers logging.config.dictConfig( { @@ -73,6 +71,3 @@ def configure_logging_two(): logger.info("Logging configured") logger.info(f"Environment: {settings.ENV}") - - -configure_logging = configure_logging_two diff --git a/src/fastapi_dynamic_response/main.py b/src/fastapi_dynamic_response/main.py index bbe1c2f..929fa3b 100644 --- a/src/fastapi_dynamic_response/main.py +++ b/src/fastapi_dynamic_response/main.py @@ -5,19 +5,21 @@ from fastapi_dynamic_response import globals from fastapi_dynamic_response.__about__ import __version__ from fastapi_dynamic_response.base.router import router as base_router from fastapi_dynamic_response.dependencies import get_content_type +from fastapi_dynamic_response.zpages.router import router as zpages_router + +from fastapi_dynamic_response.settings import settings + +from fastapi_dynamic_response.logging_config import configure_logging from fastapi_dynamic_response.middleware import ( Sitemap, add_process_time_header, catch_exceptions_middleware, log_requests, respond_based_on_content_type, + set_bound_logger, set_prefers, set_span_id, ) -from fastapi_dynamic_response.zpages.router import router as zpages_router - -from fastapi_dynamic_response.logging_config import configure_logging - configure_logging() app = FastAPI( @@ -28,7 +30,7 @@ app = FastAPI( openapi_url=None, # openapi_tags=tags_metadata, # exception_handlers=exception_handlers, - debug=True, + debug=settings.DEBUG, dependencies=[ # Depends(set_prefers), # Depends(set_span_id), @@ -47,6 +49,7 @@ app.middleware("http")(Sitemap(app)) app.middleware("http")(set_prefers) app.middleware("http")(set_span_id) app.middleware("http")(catch_exceptions_middleware) +app.middleware("http")(set_bound_logger) app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/src/fastapi_dynamic_response/middleware.py b/src/fastapi_dynamic_response/middleware.py index 74d0836..9e11ea4 100644 --- a/src/fastapi_dynamic_response/middleware.py +++ b/src/fastapi_dynamic_response/middleware.py @@ -1,4 +1,6 @@ from difflib import get_close_matches + +from fastapi_dynamic_response.settings import settings from io import BytesIO import json import time @@ -7,10 +9,6 @@ from typing import Any, Dict from uuid import uuid4 from fastapi import Request, Response -from fastapi.exceptions import ( - HTTPException as StarletteHTTPException, - RequestValidationError, -) from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse import html2text from pydantic import BaseModel, model_validator @@ -79,10 +77,16 @@ async def add_process_time_header(request: Request, call_next): start_time = time.perf_counter() response = await call_next(request) process_time = time.perf_counter() - start_time - response.headers["X-Process-Time"] = str(process_time) + if str(response.status_code)[0] in "123": + response.headers["X-Process-Time"] = str(process_time) return response +def set_bound_logger(request: Request, call_next): + request.state.bound_logger = logger.bind() + return call_next(request) + + async def set_span_id(request: Request, call_next): span_id = uuid4() request.state.span_id = span_id @@ -90,8 +94,9 @@ async def set_span_id(request: Request, call_next): response = await call_next(request) - response.headers["x-request-id"] = str(span_id) - response.headers["x-span-id"] = str(span_id) + if str(response.status_code)[0] in "123": + response.headers["x-request-id"] = str(span_id) + response.headers["x-span-id"] = str(span_id) return response @@ -121,7 +126,7 @@ def set_prefers( user_agent = request.headers.get("user-agent", "").lower() referer = request.headers.get("referer", "") - if "," in content_type: + if content_type and "," in content_type: content_type = content_type.split(",")[0] request.state.bound_logger.info( @@ -308,19 +313,6 @@ def format_json_as_rich_text(data: dict, template_name: str) -> str: return capture.get() -async def respond_based_on_content_type( - request: Request, - call_next, - content_type: str, - data: str, -): - requested_path = request.url.path - if requested_path in ["/docs", "/redoc", "/openapi.json"]: - return await call_next(request) - - return await call_next(request) - - def handle_not_found(request: Request, call_next, data: str): requested_path = request.url.path # available_routes = [route.path for route in app.router.routes if route.path] @@ -353,59 +345,44 @@ async def respond_based_on_content_type(request: Request, call_next): try: response = await call_next(request) - user_agent = request.headers.get("user-agent", "").lower() - referer = request.headers.get("referer", "") - content_type = request.query_params.get( - "content_type", - request.headers.get("content-type", request.headers.get("Accept")), - ) - # if "raw" in content_type: - # return await call_next(request) - if content_type == "*/*": - content_type = None - if ("/docs" in referer or "/redoc" in referer) and content_type is None: - content_type = "application/json" - elif is_browser_request(user_agent) and content_type is None: - content_type = "text/html" - elif is_rtf_request(user_agent) and content_type is None: - content_type = "application/rtf" - elif content_type is None: - content_type = content_type or "application/json" - - body = b"".join([chunk async for chunk in response.body_iterator]) - - data = body.decode("utf-8") - if response.status_code == 404: request.state.bound_logger.info("404 not found") - return handle_not_found( + body = b"".join([chunk async for chunk in response.body_iterator]) + data = body.decode("utf-8") + response = handle_not_found( request=request, call_next=call_next, data=data, ) - if str(response.status_code)[0] not in "123": - request.state.bound_logger.info("non-200 response") + elif str(response.status_code)[0] not in "123": + request.state.bound_logger.info(f"non-200 response {response.status_code}") + # return await handle_response(request, response, data) return response + else: + body = b"".join([chunk async for chunk in response.body_iterator]) + data = body.decode("utf-8") return await handle_response(request, response, data) - # except TemplateNotFound: - # return HTMLResponse(content="Template Not Found ", status_code=404) - except StarletteHTTPException as exc: - request.state.bound_logger.info("starlette exception") - return HTMLResponse( - content=f"Error {exc.status_code}: {exc.detail}", - status_code=exc.status_code, - ) - except RequestValidationError as exc: - request.state.bound_logger.info("request validation error") - return JSONResponse(status_code=422, content={"detail": exc.errors()}) except Exception as e: request.state.bound_logger.info("internal server error") - print(traceback.format_exc()) - return HTMLResponse(content=f"Internal Server Error: {e!s}", status_code=500) + # print(traceback.format_exc()) + raise e + if settings.ENV == "local": + return HTMLResponse( + content=f"Internal Server Error: {e!s} {traceback.format_exc()}", + status_code=500, + ) + else: + return HTMLResponse( + content=f"Internal Server Error: {e!s}", status_code=500 + ) -async def handle_response(request: Request, response: Response, data: str): +async def handle_response( + request: Request, + response: Response, + data: str, +): json_data = json.loads(data) template_name = getattr(request.state, "template_name", "default_template.html") @@ -433,7 +410,7 @@ async def handle_response(request: Request, response: Response, data: str): template = templates.get_template(template_name) html_content = template.render(data=json_data) markdown_content = html2text.html2text(html_content) - return PlainTextResponse(content=markdown_content, headers=response.headers) + return PlainTextResponse(content=markdown_content) if request.state.prefers.text: request.state.bound_logger.info("returning plain text") @@ -483,13 +460,11 @@ async def handle_response(request: Request, response: Response, data: str): # Initialize the logger async def log_requests(request: Request, call_next): # Log request details + request.state.bound_logger = logger.bind( + method=request.method, path=request.url.path + ) request.state.bound_logger.info( "Request received", - # span_id=request.state.span_id, - method=request.method, - path=request.url.path, - # headers=dict(request.headers), - # prefers=request.state.prefers, ) # logger.info( # headers=dict(request.headers), diff --git a/src/fastapi_dynamic_response/settings.py b/src/fastapi_dynamic_response/settings.py index 81fd81a..93d8680 100644 --- a/src/fastapi_dynamic_response/settings.py +++ b/src/fastapi_dynamic_response/settings.py @@ -1,8 +1,19 @@ +from pydantic import model_validator from pydantic_settings import BaseSettings class Settings(BaseSettings): ENV: str = "local" + DEBUG: bool = False class Config: env_file = "config.env" + + @model_validator(mode="after") + def validate_debug(self): + if self.ENV == "local" and self.DEBUG is False: + self.DEBUG = True + return self + + +settings = Settings()