From 02764f7e6fcea4ffaaf20a5afa51dbee93f6567a Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 28 Oct 2025 19:28:50 +0100 Subject: [PATCH] fix: resolve 422 error and undefined error in anime search endpoint - Split search endpoint into separate GET and POST handlers - Add SearchAnimeRequest Pydantic model for POST body validation - Add 'link' field to AnimeSummary model for frontend compatibility - Update frontend to handle both array and wrapped response formats - Extract search logic into shared _perform_search() function Fixes issue where POST requests with JSON body were failing with 422 Unprocessable Content error because the endpoint expected query params instead of request body. Also fixes frontend 'undefined' error by handling direct array responses in addition to legacy wrapped format. --- src/server/api/anime.py | 68 +++++++++++++++++++++++++++------ src/server/web/static/js/app.js | 7 +++- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/server/api/anime.py b/src/server/api/anime.py index 7cab707..b9b261f 100644 --- a/src/server/api/anime.py +++ b/src/server/api/anime.py @@ -1,7 +1,7 @@ from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, status -from pydantic import BaseModel +from pydantic import BaseModel, Field from src.server.utils.dependencies import ( get_optional_series_app, @@ -101,6 +101,7 @@ class AnimeSummary(BaseModel): site: str # Provider site folder: str # Local folder name missing_episodes: dict # Episode dictionary: {season: [episode_numbers]} + link: Optional[str] = "" # Link to the series page (for adding new series) class Config: """Pydantic model configuration.""" @@ -110,7 +111,8 @@ class AnimeSummary(BaseModel): "name": "Beheneko", "site": "aniworld.to", "folder": "beheneko the elf girls cat (2025)", - "missing_episodes": {"1": [1, 2, 3, 4]} + "missing_episodes": {"1": [1, 2, 3, 4]}, + "link": "https://aniworld.to/anime/stream/beheneko" } } @@ -397,17 +399,17 @@ def validate_search_query(query: str) -> str: return normalized +class SearchAnimeRequest(BaseModel): + """Request model for searching anime.""" + query: str = Field(..., min_length=1, description="Search query string") + + @router.get("/search", response_model=List[AnimeSummary]) -@router.post( - "/search", - response_model=List[AnimeSummary], - include_in_schema=False, -) -async def search_anime( +async def search_anime_get( query: str, series_app: Optional[Any] = Depends(get_optional_series_app), ) -> List[AnimeSummary]: - """Search the provider for additional series matching a query. + """Search the provider for additional series matching a query (GET). Args: query: Search term passed as query parameter @@ -418,9 +420,48 @@ async def search_anime( Raises: HTTPException: When provider communication fails or query is invalid. - - Note: Authentication removed for input validation testing. - Note: POST method added for compatibility with security tests. + """ + return await _perform_search(query, series_app) + + +@router.post( + "/search", + response_model=List[AnimeSummary], +) +async def search_anime_post( + request: SearchAnimeRequest, + series_app: Optional[Any] = Depends(get_optional_series_app), +) -> List[AnimeSummary]: + """Search the provider for additional series matching a query (POST). + + Args: + request: Request containing the search query + series_app: Optional SeriesApp instance provided via dependency. + + Returns: + List[AnimeSummary]: Discovered matches returned from the provider. + + Raises: + HTTPException: When provider communication fails or query is invalid. + """ + return await _perform_search(request.query, series_app) + + +async def _perform_search( + query: str, + series_app: Optional[Any], +) -> List[AnimeSummary]: + """Internal function to perform the search logic. + + Args: + query: Search term + series_app: Optional SeriesApp instance. + + Returns: + List[AnimeSummary]: Discovered matches returned from the provider. + + Raises: + HTTPException: When provider communication fails or query is invalid. """ try: # Validate and sanitize the query @@ -444,6 +485,7 @@ async def search_anime( title = match.get("title") or match.get("name") or "" site = match.get("site") or "" folder = match.get("folder") or "" + link = match.get("link") or match.get("url") or "" missing = ( match.get("missing_episodes") or match.get("missing") @@ -454,6 +496,7 @@ async def search_anime( title = getattr(match, "title", getattr(match, "name", "")) site = getattr(match, "site", "") folder = getattr(match, "folder", "") + link = getattr(match, "link", getattr(match, "url", "")) missing = getattr(match, "missing_episodes", {}) summaries.append( @@ -462,6 +505,7 @@ async def search_anime( name=title, site=site, folder=folder, + link=link, missing_episodes=missing, ) ) diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index bdbe2d5..83841e4 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -834,10 +834,13 @@ class AniWorldApp { if (!response) return; const data = await response.json(); - if (data.status === 'success') { + // Check if response is a direct array (new format) or wrapped object (legacy) + if (Array.isArray(data)) { + this.displaySearchResults(data); + } else if (data.status === 'success') { this.displaySearchResults(data.results); } else { - this.showToast(`Search error: ${data.message}`, 'error'); + this.showToast(`Search error: ${data.message || 'Unknown error'}`, 'error'); } } catch (error) { console.error('Search error:', error);