fix: escape html and check render errors in anime handlers

This commit is contained in:
2026-04-21 01:20:32 +02:00
parent 8242d0eaf9
commit 06b36382a6

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"html"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@@ -43,12 +44,15 @@ type quickSearchResult struct {
func renderNotFoundPage(r *http.Request, w http.ResponseWriter) { func renderNotFoundPage(r *http.Request, w http.ResponseWriter) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
templates.NotFoundPage().Render(r.Context(), w) if err := templates.NotFoundPage().Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func writeInlineLoadError(w http.ResponseWriter, message string) { func writeInlineLoadError(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<p style="color: var(--text-muted); font-size: var(--text-sm);">` + message + `</p>`)) _, _ = w.Write([]byte(`<p style="color: var(--text-muted); font-size: var(--text-sm);">` + html.EscapeString(message) + `</p>`))
} }
func parsePageParam(r *http.Request) int { func parsePageParam(r *http.Request) int {
@@ -78,7 +82,10 @@ func (h *Handler) HandleCatalog(w http.ResponseWriter, r *http.Request) {
renderNotFoundPage(r, w) renderNotFoundPage(r, w)
return return
} }
templates.Catalog().Render(r.Context(), w) if err := templates.Catalog().Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) watchlistMap(ctx context.Context, userID string) map[int]string { func (h *Handler) watchlistMap(ctx context.Context, userID string) map[int]string {
@@ -101,7 +108,11 @@ func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q") query := r.URL.Query().Get("q")
if query == "" { if query == "" {
templates.Search("").Render(r.Context(), w) if err := templates.Search("").Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return return
} }
@@ -117,11 +128,18 @@ func (h *Handler) HandleSearch(w http.ResponseWriter, r *http.Request) {
return return
} }
statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) statuses := h.watchlistMap(r.Context(), userIDFromRequest(r))
templates.SearchResultsWrapper(query, res.Animes, statuses, 2, res.HasNextPage).Render(r.Context(), w) if err := templates.SearchResultsWrapper(query, res.Animes, statuses, 2, res.HasNextPage).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return return
} }
templates.Search(query).Render(r.Context(), w) if err := templates.Search(query).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) {
@@ -142,7 +160,10 @@ func (h *Handler) HandleAPISearch(w http.ResponseWriter, r *http.Request) {
res.Animes = deduplicateAnimes(res.Animes) res.Animes = deduplicateAnimes(res.Animes)
statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) statuses := h.watchlistMap(r.Context(), userIDFromRequest(r))
templates.SearchItems(query, res.Animes, statuses, page+1, res.HasNextPage).Render(r.Context(), w) if err := templates.SearchItems(query, res.Animes, statuses, page+1, res.HasNextPage).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) {
@@ -152,12 +173,20 @@ func (h *Handler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) {
if err == nil { if err == nil {
result.Animes = deduplicateAnimes(result.Animes) result.Animes = deduplicateAnimes(result.Animes)
statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) statuses := h.watchlistMap(r.Context(), userIDFromRequest(r))
templates.CatalogItems(result.Animes, statuses, page+1, result.HasNextPage).Render(r.Context(), w) if err := templates.CatalogItems(result.Animes, statuses, page+1, result.HasNextPage).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return return
} }
if jikan.IsRetryableError(err) { if jikan.IsRetryableError(err) {
templates.CatalogPlaceholderItems(25).Render(r.Context(), w) if err := templates.CatalogPlaceholderItems(25).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return return
} }
@@ -184,7 +213,11 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
h.jikanClient.EnqueueAnimeFetchRetry(r.Context(), id, err) h.jikanClient.EnqueueAnimeFetchRetry(r.Context(), id, err)
if jikan.IsRetryableError(err) { if jikan.IsRetryableError(err) {
animecomponents.Pending(id).Render(r.Context(), w) if err := animecomponents.Pending(id).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return return
} }
@@ -211,7 +244,10 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) {
} }
} }
templates.AnimeDetails(anime, currentStatus, nextEpisode).Render(r.Context(), w) if err := templates.AnimeDetails(anime, currentStatus, nextEpisode).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
@@ -241,7 +277,11 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
writeInlineLoadError(w, "Failed to load relations.") writeInlineLoadError(w, "Failed to load relations.")
return return
} }
animecomponents.RelationsList(relations, statuses).Render(r.Context(), w) if err := animecomponents.RelationsList(relations, statuses).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
case "recommendations": case "recommendations":
recs, err := h.jikanClient.GetRecommendations(r.Context(), id, 12) recs, err := h.jikanClient.GetRecommendations(r.Context(), id, 12)
if err != nil { if err != nil {
@@ -249,7 +289,11 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
writeInlineLoadError(w, "Failed to load recommendations.") writeInlineLoadError(w, "Failed to load recommendations.")
return return
} }
animecomponents.Recommendations(recs, statuses).Render(r.Context(), w) if err := animecomponents.Recommendations(recs, statuses).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
case "episodes": case "episodes":
currentEpisode := r.URL.Query().Get("current") currentEpisode := r.URL.Query().Get("current")
episodes, err := h.getEpisodes(r.Context(), id) episodes, err := h.getEpisodes(r.Context(), id)
@@ -258,7 +302,11 @@ func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
writeInlineLoadError(w, "Failed to load episodes.") writeInlineLoadError(w, "Failed to load episodes.")
return return
} }
watchcomponents.EpisodeList(episodes, currentEpisode, id).Render(r.Context(), w) if err := watchcomponents.EpisodeList(episodes, currentEpisode, id).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
default: default:
renderNotFoundPage(r, w) renderNotFoundPage(r, w)
} }
@@ -282,7 +330,10 @@ func (h *Handler) HandleAPIEpisodes(w http.ResponseWriter, r *http.Request) {
return return
} }
watchcomponents.EpisodeList(episodes, currentEpisode, id).Render(r.Context(), w) if err := watchcomponents.EpisodeList(episodes, currentEpisode, id).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) getEpisodes(ctx context.Context, animeID int) ([]jikan.Episode, error) { func (h *Handler) getEpisodes(ctx context.Context, animeID int) ([]jikan.Episode, error) {
@@ -348,7 +399,10 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
} }
func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) {
templates.Discover().Render(r.Context(), w) if err := templates.Discover().Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request) {
@@ -364,7 +418,10 @@ func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request
res.Animes = deduplicateAnimes(res.Animes) res.Animes = deduplicateAnimes(res.Animes)
statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) statuses := h.watchlistMap(r.Context(), userIDFromRequest(r))
templates.DiscoverItems(res.Animes, statuses, "airing", page+1, res.HasNextPage).Render(r.Context(), w) if err := templates.DiscoverItems(res.Animes, statuses, "airing", page+1, res.HasNextPage).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Request) {
@@ -380,7 +437,10 @@ func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Reque
res.Animes = deduplicateAnimes(res.Animes) res.Animes = deduplicateAnimes(res.Animes)
statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) statuses := h.watchlistMap(r.Context(), userIDFromRequest(r))
templates.DiscoverItems(res.Animes, statuses, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w) if err := templates.DiscoverItems(res.Animes, statuses, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) HandleStudioDetails(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleStudioDetails(w http.ResponseWriter, r *http.Request) {
@@ -408,7 +468,11 @@ func (h *Handler) HandleStudioDetails(w http.ResponseWriter, r *http.Request) {
log.Printf("studio anime fetch error for %d: %v", id, err) log.Printf("studio anime fetch error for %d: %v", id, err)
if jikan.IsRetryableError(err) || errors.Is(err, context.Canceled) { if jikan.IsRetryableError(err) || errors.Is(err, context.Canceled) {
// Render page with empty anime list if API is rate limiting // Render page with empty anime list if API is rate limiting
templates.StudioDetails(producer, []jikan.Anime{}, nil, false, 2).Render(r.Context(), w) if err := templates.StudioDetails(producer, []jikan.Anime{}, nil, false, 2).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return return
} }
http.Error(w, "Failed to fetch studio anime", http.StatusInternalServerError) http.Error(w, "Failed to fetch studio anime", http.StatusInternalServerError)
@@ -416,7 +480,10 @@ func (h *Handler) HandleStudioDetails(w http.ResponseWriter, r *http.Request) {
} }
statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) statuses := h.watchlistMap(r.Context(), userIDFromRequest(r))
templates.StudioDetails(producer, result.Animes, statuses, result.HasNextPage, 2).Render(r.Context(), w) if err := templates.StudioDetails(producer, result.Animes, statuses, result.HasNextPage, 2).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }
func (h *Handler) HandleAPIStudioAnime(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleAPIStudioAnime(w http.ResponseWriter, r *http.Request) {
@@ -450,5 +517,8 @@ func (h *Handler) HandleAPIStudioAnime(w http.ResponseWriter, r *http.Request) {
result.Animes = deduplicateAnimes(result.Animes) result.Animes = deduplicateAnimes(result.Animes)
statuses := h.watchlistMap(r.Context(), userIDFromRequest(r)) statuses := h.watchlistMap(r.Context(), userIDFromRequest(r))
templates.StudioAnimeItems(result.Animes, statuses, result.HasNextPage, id, page+1).Render(r.Context(), w) if err := templates.StudioAnimeItems(result.Animes, statuses, result.HasNextPage, id, page+1).Render(r.Context(), w); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
} }