Put app/routers/assets.py
Browse files- app/routers/assets.py +9 -19
app/routers/assets.py
CHANGED
|
@@ -1,16 +1,4 @@
|
|
| 1 |
-
"""POST /assets β extract image/video/audio/svg-icon/font assets from a URL.
|
| 2 |
-
|
| 3 |
-
Returns structured error responses for all failure modes:
|
| 4 |
-
- Bad request (400) β missing/invalid url
|
| 5 |
-
- SSRF block (403) β blocked host
|
| 6 |
-
- Not HTML (415) β wrong content-type
|
| 7 |
-
- Fetch error (502) β network or redirect failure
|
| 8 |
-
- Page too large (413) β exceeds size cap
|
| 9 |
-
- Server error (500) β unexpected
|
| 10 |
-
|
| 11 |
-
All errors are logged at INFO+ level and returned as JSON with
|
| 12 |
-
the request ID attached.
|
| 13 |
-
"""
|
| 14 |
|
| 15 |
import logging
|
| 16 |
import uuid
|
|
@@ -19,12 +7,12 @@ from fastapi import APIRouter, HTTPException, status
|
|
| 19 |
|
| 20 |
from app.schemas import AssetExtractRequest, AssetExtractResponse, ErrorResponse
|
| 21 |
from app.services.asset_service import AssetExtractError, extract_assets
|
|
|
|
| 22 |
|
| 23 |
router = APIRouter()
|
| 24 |
|
| 25 |
|
| 26 |
def _log_request(req: AssetExtractRequest, phase: str, **extra) -> None:
|
| 27 |
-
"""Log a structured request entry."""
|
| 28 |
logger = logging.getLogger(__name__)
|
| 29 |
logger.info("request phase=%s id=%s url=%r groups=%r enrich=%s include_data_uris=%s extra=%r",
|
| 30 |
phase, req._request_id or "?", req.url, req.groups, req.enrich,
|
|
@@ -32,7 +20,6 @@ def _log_request(req: AssetExtractRequest, phase: str, **extra) -> None:
|
|
| 32 |
|
| 33 |
|
| 34 |
def _log_response(req: AssetExtractRequest, status_code: int, phase: str, **extra) -> None:
|
| 35 |
-
"""Log a structured response entry."""
|
| 36 |
logger = logging.getLogger(__name__)
|
| 37 |
logger.info("response phase=%s status=%s id=%s url=%r extra=%r",
|
| 38 |
phase, status_code, req._request_id or "?", req.url, extra)
|
|
@@ -43,7 +30,6 @@ async def extract_assets_endpoint(req: AssetExtractRequest) -> AssetExtractRespo
|
|
| 43 |
"""Extract assets from a URL, grouped by category."""
|
| 44 |
_log_request(req, "start")
|
| 45 |
req_id = req._request_id or str(uuid.uuid4())
|
| 46 |
-
_request_id = req_id
|
| 47 |
try:
|
| 48 |
result = await extract_assets(
|
| 49 |
url=req.url,
|
|
@@ -55,6 +41,9 @@ async def extract_assets_endpoint(req: AssetExtractRequest) -> AssetExtractRespo
|
|
| 55 |
return result
|
| 56 |
except AssetExtractError as exc:
|
| 57 |
_log_response(req, 502, "asset_extract_error", detail=str(exc))
|
|
|
|
|
|
|
|
|
|
| 58 |
raise HTTPException(
|
| 59 |
status_code=status.HTTP_502_BAD_GATEWAY,
|
| 60 |
detail=ErrorResponse(
|
|
@@ -62,7 +51,8 @@ async def extract_assets_endpoint(req: AssetExtractRequest) -> AssetExtractRespo
|
|
| 62 |
code=502,
|
| 63 |
detail=str(exc),
|
| 64 |
request_id=req_id,
|
| 65 |
-
|
|
|
|
| 66 |
) from exc
|
| 67 |
except Exception as exc:
|
| 68 |
_log_response(req, 500, "unexpected_error", detail=str(exc))
|
|
@@ -73,10 +63,10 @@ async def extract_assets_endpoint(req: AssetExtractRequest) -> AssetExtractRespo
|
|
| 73 |
code=500,
|
| 74 |
detail=str(exc),
|
| 75 |
request_id=req_id,
|
| 76 |
-
).
|
| 77 |
) from exc
|
| 78 |
|
| 79 |
|
| 80 |
@router.get("/health")
|
| 81 |
async def health_check() -> dict[str, str]:
|
| 82 |
-
return {"status": "ok", "endpoint": "/assets"}
|
|
|
|
| 1 |
+
"""POST /assets β extract image/video/audio/svg-icon/font assets from a URL."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import logging
|
| 4 |
import uuid
|
|
|
|
| 7 |
|
| 8 |
from app.schemas import AssetExtractRequest, AssetExtractResponse, ErrorResponse
|
| 9 |
from app.services.asset_service import AssetExtractError, extract_assets
|
| 10 |
+
from app.services.fetcher import FetchError
|
| 11 |
|
| 12 |
router = APIRouter()
|
| 13 |
|
| 14 |
|
| 15 |
def _log_request(req: AssetExtractRequest, phase: str, **extra) -> None:
|
|
|
|
| 16 |
logger = logging.getLogger(__name__)
|
| 17 |
logger.info("request phase=%s id=%s url=%r groups=%r enrich=%s include_data_uris=%s extra=%r",
|
| 18 |
phase, req._request_id or "?", req.url, req.groups, req.enrich,
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def _log_response(req: AssetExtractRequest, status_code: int, phase: str, **extra) -> None:
|
|
|
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
logger.info("response phase=%s status=%s id=%s url=%r extra=%r",
|
| 25 |
phase, status_code, req._request_id or "?", req.url, extra)
|
|
|
|
| 30 |
"""Extract assets from a URL, grouped by category."""
|
| 31 |
_log_request(req, "start")
|
| 32 |
req_id = req._request_id or str(uuid.uuid4())
|
|
|
|
| 33 |
try:
|
| 34 |
result = await extract_assets(
|
| 35 |
url=req.url,
|
|
|
|
| 41 |
return result
|
| 42 |
except AssetExtractError as exc:
|
| 43 |
_log_response(req, 502, "asset_extract_error", detail=str(exc))
|
| 44 |
+
stages = None
|
| 45 |
+
if hasattr(exc, '__cause__') and isinstance(exc.__cause__, FetchError):
|
| 46 |
+
stages = exc.__cause__.stages
|
| 47 |
raise HTTPException(
|
| 48 |
status_code=status.HTTP_502_BAD_GATEWAY,
|
| 49 |
detail=ErrorResponse(
|
|
|
|
| 51 |
code=502,
|
| 52 |
detail=str(exc),
|
| 53 |
request_id=req_id,
|
| 54 |
+
stages=stages,
|
| 55 |
+
).model_dump(),
|
| 56 |
) from exc
|
| 57 |
except Exception as exc:
|
| 58 |
_log_response(req, 500, "unexpected_error", detail=str(exc))
|
|
|
|
| 63 |
code=500,
|
| 64 |
detail=str(exc),
|
| 65 |
request_id=req_id,
|
| 66 |
+
).model_dump(),
|
| 67 |
) from exc
|
| 68 |
|
| 69 |
|
| 70 |
@router.get("/health")
|
| 71 |
async def health_check() -> dict[str, str]:
|
| 72 |
+
return {"status": "ok", "endpoint": "/assets"}
|