diff --git a/api/anime/handler.go b/api/anime/handler.go index 9342ae5..edb211b 100644 --- a/api/anime/handler.go +++ b/api/anime/handler.go @@ -29,7 +29,7 @@ type quickSearchResult struct { func renderNotFoundPage(r *http.Request, w http.ResponseWriter) { w.WriteHeader(http.StatusNotFound) - if err := templates.GetRenderer().ExecuteTemplate(w, "not_found.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "not_found.gohtml", map[string]any{ "CurrentPath": r.URL.Path, }); err != nil { log.Printf("render error: %v", err) @@ -98,7 +98,7 @@ func (h *Handler) HandleCatalog(w http.ResponseWriter, r *http.Request) { } } - if err := templates.GetRenderer().ExecuteTemplate(w, "index.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "index.gohtml", map[string]any{ "MostPopular": animes.Animes, "CurrentlyAiring": currentlyAiring.Animes, "ContinueWatching": cw, @@ -150,7 +150,7 @@ func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "text/html") - err := templates.GetRenderer().ExecuteFragment(w, "browse.gohtml", "anime_card_scroll", map[string]any{ + err := templates.GetRenderer().ExecuteFragment(r.Context(), w, "browse.gohtml", "anime_card_scroll", map[string]any{ "Animes": res.Animes, "NextPage": page + 1, "HasNextPage": res.HasNextPage, @@ -184,7 +184,7 @@ func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) { } } - if err := templates.GetRenderer().ExecuteTemplate(w, "browse.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "browse.gohtml", map[string]any{ "User": user, "CurrentPath": r.URL.Path, "Query": q, @@ -251,7 +251,7 @@ func (h *Handler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) { } } - if err := templates.GetRenderer().ExecuteTemplate(w, "anime.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "anime.gohtml", map[string]any{ "Anime": anime, "User": user, "Status": status, @@ -287,7 +287,7 @@ func (h *Handler) HandleHTMLWatchOrder(w http.ResponseWriter, r *http.Request) { } } - if err := templates.GetRenderer().ExecuteFragment(w, "anime.gohtml", "watch_order", map[string]any{ + if err := templates.GetRenderer().ExecuteFragment(r.Context(), w, "anime.gohtml", "watch_order", map[string]any{ "Relations": relations, "AnimeID": id, "WatchlistMap": watchlistMap, @@ -390,7 +390,7 @@ func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) { } } - if err := templates.GetRenderer().ExecuteTemplate(w, "discover.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "discover.gohtml", map[string]any{ "User": user, "CurrentPath": r.URL.Path, "Trending": uniqueTrending, diff --git a/api/auth/handler.go b/api/auth/handler.go index 557e04b..c327993 100644 --- a/api/auth/handler.go +++ b/api/auth/handler.go @@ -25,7 +25,7 @@ func rateLimitErrorFromQuery(r *http.Request) string { } func (h *Handler) HandleLoginPage(w http.ResponseWriter, r *http.Request) { - if err := templates.GetRenderer().ExecuteTemplate(w, "login.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "login.gohtml", map[string]any{ "CurrentPath": r.URL.Path, }); err != nil { log.Printf("render error: %v", err) @@ -34,7 +34,7 @@ func (h *Handler) HandleLoginPage(w http.ResponseWriter, r *http.Request) { func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { - templates.GetRenderer().ExecuteTemplate(w, "login.gohtml", map[string]any{ + templates.GetRenderer().ExecuteTemplate(r.Context(), w, "login.gohtml", map[string]any{ "Error": "Something went wrong. Please try again.", "Username": "", "CurrentPath": r.URL.Path, @@ -46,7 +46,7 @@ func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") if username == "" || password == "" { - templates.GetRenderer().ExecuteTemplate(w, "login.gohtml", map[string]any{ + templates.GetRenderer().ExecuteTemplate(r.Context(), w, "login.gohtml", map[string]any{ "Error": "The email or password is wrong.", "Username": username, "CurrentPath": r.URL.Path, @@ -56,7 +56,7 @@ func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) { session, err := h.authService.Login(r.Context(), username, password) if err != nil { - templates.GetRenderer().ExecuteTemplate(w, "login.gohtml", map[string]any{ + templates.GetRenderer().ExecuteTemplate(r.Context(), w, "login.gohtml", map[string]any{ "Error": "The email or password is wrong.", "Username": username, "CurrentPath": r.URL.Path, diff --git a/api/playback/handler.go b/api/playback/handler.go index 3d3e912..32be3ce 100644 --- a/api/playback/handler.go +++ b/api/playback/handler.go @@ -30,7 +30,7 @@ func NewHandler(svc *Service, jikanClient *jikan.Client) *Handler { func renderNotFoundPage(r *http.Request, w http.ResponseWriter) { w.WriteHeader(http.StatusNotFound) - if err := templates.GetRenderer().ExecuteTemplate(w, "not_found.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "not_found.gohtml", map[string]any{ "CurrentPath": r.URL.Path, }); err != nil { log.Printf("render error: %v", err) @@ -174,7 +174,7 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) { } } - if err := templates.GetRenderer().ExecuteTemplate(w, "watch.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "watch.gohtml", map[string]any{ "Anime": anime, "Episodes": episodes.Data, "WatchData": watchData, diff --git a/api/watchlist/handler.go b/api/watchlist/handler.go index c5df46c..172a58d 100644 --- a/api/watchlist/handler.go +++ b/api/watchlist/handler.go @@ -115,7 +115,7 @@ func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) { entries, err := h.service.GetUserWatchlist(r.Context(), user.ID) if err != nil { log.Printf("failed to fetch watchlist: %v", err) - if err := templates.GetRenderer().ExecuteTemplate(w, "not_found.gohtml", map[string]any{ + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "not_found.gohtml", map[string]any{ "CurrentPath": r.URL.Path, }); err != nil { log.Printf("render error: %v", err) @@ -158,7 +158,7 @@ func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) { templateName = "watchlist_partial.gohtml" } - if err := templates.GetRenderer().ExecuteTemplate(w, templateName, data); err != nil { + if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, templateName, data); err != nil { log.Printf("render error: %v", err) } } diff --git a/integrations/jikan/client.go b/integrations/jikan/client.go index bcfda02..ec37db3 100644 --- a/integrations/jikan/client.go +++ b/integrations/jikan/client.go @@ -27,7 +27,15 @@ type Client struct { func NewClient(db database.Querier) *Client { return &Client{ - httpClient: &http.Client{Timeout: 10 * time.Second}, + httpClient: &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + DisableKeepAlives: false, + TLSHandshakeTimeout: 5 * time.Second, + }, + }, baseURL: "https://api.jikan.moe/v4", db: db, retrySignal: make(chan struct{}, 1), @@ -284,6 +292,12 @@ func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Dur func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) error { maxRetries := 5 for attempt := range maxRetries { + select { + case <-ctx.Done(): + return fmt.Errorf("request canceled while retrying jikan request: %w", ctx.Err()) + default: + } + if err := c.waitRateLimit(ctx); err != nil { return err } @@ -295,6 +309,9 @@ func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) err resp, err := c.httpClient.Do(req) if err != nil { + if errors.Is(err, context.Canceled) { + return fmt.Errorf("request canceled while retrying jikan request: %w", err) + } if attempt < maxRetries-1 && IsRetryableError(err) { if retryErr := waitForRetry(ctx, retryDelay(attempt)); retryErr != nil { return retryErr diff --git a/templates/renderer.go b/templates/renderer.go index 2981441..b249b2e 100644 --- a/templates/renderer.go +++ b/templates/renderer.go @@ -1,6 +1,7 @@ package templates import ( + "context" "encoding/json" "fmt" "html/template" @@ -114,7 +115,13 @@ func GetRenderer() *Renderer { return renderer } -func (r *Renderer) ExecuteTemplate(wr io.Writer, name string, data any) error { +func (r *Renderer) ExecuteTemplate(ctx context.Context, wr io.Writer, name string, data any) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + tmpl, ok := r.templates[name] if !ok { return fmt.Errorf("template %s not found", name) @@ -122,7 +129,13 @@ func (r *Renderer) ExecuteTemplate(wr io.Writer, name string, data any) error { return tmpl.ExecuteTemplate(wr, "base.gohtml", data) } -func (r *Renderer) ExecuteFragment(wr io.Writer, name string, block string, data any) error { +func (r *Renderer) ExecuteFragment(ctx context.Context, wr io.Writer, name string, block string, data any) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + tmpl, ok := r.templates[name] if !ok { return fmt.Errorf("template %s not found", name)