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.
This commit is contained in:
Lukas 2025-10-28 19:28:50 +01:00
parent 95b7059576
commit 02764f7e6f
2 changed files with 61 additions and 14 deletions

View File

@ -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,
)
)

View File

@ -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);