diff --git a/internal/anime/handler/handler.go b/internal/anime/handler/handler.go index d13a9d2..22d0f39 100644 --- a/internal/anime/handler/handler.go +++ b/internal/anime/handler/handler.go @@ -7,6 +7,7 @@ import ( "mal/internal/db" "mal/internal/domain" "mal/internal/observability" + "mal/internal/server" "net/http" "net/url" "strconv" @@ -75,11 +76,19 @@ func (h *AnimeHandler) Register(r *gin.Engine) { func (h *AnimeHandler) HandleProducers(c *gin.Context) { q := strings.TrimSpace(c.Query("q")) - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid page") + return + } if page < 1 { page = 1 } - limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + limit, err := strconv.Atoi(c.DefaultQuery("limit", "50")) + if err != nil { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid limit") + return + } if limit < 1 { limit = 12 } @@ -113,7 +122,15 @@ func (h *AnimeHandler) HandleProducers(c *gin.Context) { return } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + server.RespondError( + c, + http.StatusInternalServerError, + "producers_fetch_failed", + "anime", + "failed to load producers", + map[string]any{"q": q, "page": page, "limit": limit}, + err, + ) return } @@ -329,27 +346,57 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) { orderBy := c.Query("order_by") sort := c.Query("sort") sfw := c.Query("sfw") != "false" - studioID, _ := strconv.Atoi(c.Query("studio")) - if studioID < 0 { - studioID = 0 + studioID := 0 + if raw := strings.TrimSpace(c.Query("studio")); raw != "" { + id, err := strconv.Atoi(raw) + if err != nil || id < 0 { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid studio id") + return + } + studioID = id } var genres []int for _, g := range c.QueryArray("genres") { - id, _ := strconv.Atoi(g) + id, err := strconv.Atoi(g) + if err != nil { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid genre id") + return + } if id > 0 { genres = append(genres, id) } } - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid page") + return + } if page < 1 { page = 1 } res, err := h.svc.SearchAdvanced(c.Request.Context(), q, animeType, status, orderBy, sort, genres, studioID, sfw, page, 24) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + server.RespondError( + c, + http.StatusInternalServerError, + "browse_search_failed", + "anime", + "failed to load browse results", + map[string]any{ + "q": q, + "type": animeType, + "status": status, + "order_by": orderBy, + "sort": sort, + "studio": studioID, + "sfw": sfw, + "page": page, + }, + err, + ) return } @@ -434,9 +481,9 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) { } func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) { - id, _ := strconv.Atoi(c.Param("id")) - if id <= 0 { - c.Status(http.StatusNotFound) + id, err := strconv.Atoi(c.Param("id")) + if err != nil || id <= 0 { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid anime id") return } @@ -523,9 +570,9 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) { } func (h *AnimeHandler) HandleHTMLWatchOrder(c *gin.Context) { - id, _ := strconv.Atoi(c.Query("animeId")) - if id <= 0 { - c.Status(http.StatusBadRequest) + id, err := strconv.Atoi(c.Query("animeId")) + if err != nil || id <= 0 { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid anime id") return } @@ -802,11 +849,19 @@ func (h *AnimeHandler) HandleRandomAnime(c *gin.Context) { anime, err := h.svc.GetRandomAnime(ctx) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch random anime"}) + server.RespondError( + c, + http.StatusInternalServerError, + "random_anime_fetch_failed", + "anime", + "failed to fetch random anime", + nil, + err, + ) return } if anime.MalID == 0 { - c.JSON(http.StatusBadGateway, gin.H{"error": "Random anime unavailable"}) + server.RespondHTMLOrJSONError(c, http.StatusBadGateway, "random anime unavailable") return } @@ -824,20 +879,32 @@ func (h *AnimeHandler) HandleRandomAnime(c *gin.Context) { } func (h *AnimeHandler) HandleAnimeReviews(c *gin.Context) { - id, _ := strconv.Atoi(c.Param("id")) - if id <= 0 { - c.Status(http.StatusNotFound) + id, err := strconv.Atoi(c.Param("id")) + if err != nil || id <= 0 { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid anime id") return } - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid page") + return + } if page < 1 { page = 1 } reviews, hasNextPage, err := h.svc.GetReviews(c.Request.Context(), id, page) if err != nil { - c.Status(http.StatusInternalServerError) + server.RespondError( + c, + http.StatusInternalServerError, + "anime_reviews_fetch_failed", + "anime", + "failed to load reviews", + map[string]any{"anime_id": id, "page": page}, + err, + ) return } diff --git a/internal/watchlist/handler/handler.go b/internal/watchlist/handler/handler.go index 2066a80..2b0608a 100644 --- a/internal/watchlist/handler/handler.go +++ b/internal/watchlist/handler/handler.go @@ -2,6 +2,7 @@ package handler import ( "mal/internal/domain" + "mal/internal/server" "net/http" "strconv" @@ -35,13 +36,21 @@ func (h *WatchlistHandler) HandleUpdateWatchlist(c *gin.Context) { Status string `json:"status"` } if err := c.ShouldBindJSON(&body); err != nil || body.AnimeID <= 0 || body.Status == "" { - c.Status(http.StatusBadRequest) + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid request body") return } err := h.svc.UpdateEntry(c.Request.Context(), userID, body.AnimeID, body.Status) if err != nil { - c.Status(http.StatusInternalServerError) + server.RespondError( + c, + http.StatusInternalServerError, + "watchlist_update_failed", + "watchlist", + "failed to update watchlist entry", + map[string]any{"user_id": userID, "anime_id": body.AnimeID, "status": body.Status}, + err, + ) return } @@ -55,16 +64,24 @@ func (h *WatchlistHandler) HandleDeleteWatchlist(c *gin.Context) { userID = u.ID } - animeID, _ := strconv.ParseInt(c.Param("id"), 10, 64) + animeID, err := strconv.ParseInt(c.Param("id"), 10, 64) - if animeID <= 0 { - c.Status(http.StatusBadRequest) + if err != nil || animeID <= 0 { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid anime id") return } - err := h.svc.RemoveEntry(c.Request.Context(), userID, animeID) + err = h.svc.RemoveEntry(c.Request.Context(), userID, animeID) if err != nil { - c.Status(http.StatusInternalServerError) + server.RespondError( + c, + http.StatusInternalServerError, + "watchlist_remove_failed", + "watchlist", + "failed to remove watchlist entry", + map[string]any{"user_id": userID, "anime_id": animeID}, + err, + ) return } @@ -78,16 +95,24 @@ func (h *WatchlistHandler) HandleDeleteContinueWatching(c *gin.Context) { userID = u.ID } - animeID, _ := strconv.ParseInt(c.Param("id"), 10, 64) + animeID, err := strconv.ParseInt(c.Param("id"), 10, 64) - if animeID <= 0 { - c.Status(http.StatusBadRequest) + if err != nil || animeID <= 0 { + server.RespondHTMLOrJSONError(c, http.StatusBadRequest, "invalid anime id") return } - err := h.svc.DeleteContinueWatching(c.Request.Context(), userID, animeID) + err = h.svc.DeleteContinueWatching(c.Request.Context(), userID, animeID) if err != nil { - c.Status(http.StatusInternalServerError) + server.RespondError( + c, + http.StatusInternalServerError, + "continue_watching_delete_failed", + "watchlist", + "failed to delete continue watching entry", + map[string]any{"user_id": userID, "anime_id": animeID}, + err, + ) return } @@ -103,7 +128,15 @@ func (h *WatchlistHandler) HandleGetWatchlist(c *gin.Context) { entries, err := h.svc.GetWatchlist(c.Request.Context(), userID) if err != nil { - c.Status(http.StatusInternalServerError) + server.RespondError( + c, + http.StatusInternalServerError, + "watchlist_load_failed", + "watchlist", + "failed to load watchlist", + map[string]any{"user_id": userID}, + err, + ) return }