fix: resolve jikan api eof and context cancellation errors
- add http transport with connection pooling to prevent stale connections - check ctx.Done() before each retry attempt to abort early - pass context to renderer to skip writing on canceled requests
This commit is contained in:
@@ -29,7 +29,7 @@ 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)
|
||||||
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,
|
"CurrentPath": r.URL.Path,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("render error: %v", err)
|
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,
|
"MostPopular": animes.Animes,
|
||||||
"CurrentlyAiring": currentlyAiring.Animes,
|
"CurrentlyAiring": currentlyAiring.Animes,
|
||||||
"ContinueWatching": cw,
|
"ContinueWatching": cw,
|
||||||
@@ -150,7 +150,7 @@ func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
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,
|
"Animes": res.Animes,
|
||||||
"NextPage": page + 1,
|
"NextPage": page + 1,
|
||||||
"HasNextPage": res.HasNextPage,
|
"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,
|
"User": user,
|
||||||
"CurrentPath": r.URL.Path,
|
"CurrentPath": r.URL.Path,
|
||||||
"Query": q,
|
"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,
|
"Anime": anime,
|
||||||
"User": user,
|
"User": user,
|
||||||
"Status": status,
|
"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,
|
"Relations": relations,
|
||||||
"AnimeID": id,
|
"AnimeID": id,
|
||||||
"WatchlistMap": watchlistMap,
|
"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,
|
"User": user,
|
||||||
"CurrentPath": r.URL.Path,
|
"CurrentPath": r.URL.Path,
|
||||||
"Trending": uniqueTrending,
|
"Trending": uniqueTrending,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func rateLimitErrorFromQuery(r *http.Request) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) HandleLoginPage(w http.ResponseWriter, r *http.Request) {
|
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,
|
"CurrentPath": r.URL.Path,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("render error: %v", err)
|
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) {
|
func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.ParseForm(); err != nil {
|
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.",
|
"Error": "Something went wrong. Please try again.",
|
||||||
"Username": "",
|
"Username": "",
|
||||||
"CurrentPath": r.URL.Path,
|
"CurrentPath": r.URL.Path,
|
||||||
@@ -46,7 +46,7 @@ func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
if username == "" || 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.",
|
"Error": "The email or password is wrong.",
|
||||||
"Username": username,
|
"Username": username,
|
||||||
"CurrentPath": r.URL.Path,
|
"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)
|
session, err := h.authService.Login(r.Context(), username, password)
|
||||||
if err != nil {
|
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.",
|
"Error": "The email or password is wrong.",
|
||||||
"Username": username,
|
"Username": username,
|
||||||
"CurrentPath": r.URL.Path,
|
"CurrentPath": r.URL.Path,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewHandler(svc *Service, jikanClient *jikan.Client) *Handler {
|
|||||||
|
|
||||||
func renderNotFoundPage(r *http.Request, w http.ResponseWriter) {
|
func renderNotFoundPage(r *http.Request, w http.ResponseWriter) {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
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,
|
"CurrentPath": r.URL.Path,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("render error: %v", err)
|
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,
|
"Anime": anime,
|
||||||
"Episodes": episodes.Data,
|
"Episodes": episodes.Data,
|
||||||
"WatchData": watchData,
|
"WatchData": watchData,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) {
|
|||||||
entries, err := h.service.GetUserWatchlist(r.Context(), user.ID)
|
entries, err := h.service.GetUserWatchlist(r.Context(), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to fetch watchlist: %v", err)
|
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,
|
"CurrentPath": r.URL.Path,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("render error: %v", err)
|
log.Printf("render error: %v", err)
|
||||||
@@ -158,7 +158,7 @@ func (h *Handler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) {
|
|||||||
templateName = "watchlist_partial.gohtml"
|
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)
|
log.Printf("render error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,15 @@ type Client struct {
|
|||||||
|
|
||||||
func NewClient(db database.Querier) *Client {
|
func NewClient(db database.Querier) *Client {
|
||||||
return &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",
|
baseURL: "https://api.jikan.moe/v4",
|
||||||
db: db,
|
db: db,
|
||||||
retrySignal: make(chan struct{}, 1),
|
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 {
|
func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) error {
|
||||||
maxRetries := 5
|
maxRetries := 5
|
||||||
for attempt := range maxRetries {
|
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 {
|
if err := c.waitRateLimit(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -295,6 +309,9 @@ func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) err
|
|||||||
|
|
||||||
resp, err := c.httpClient.Do(req)
|
resp, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
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 attempt < maxRetries-1 && IsRetryableError(err) {
|
||||||
if retryErr := waitForRetry(ctx, retryDelay(attempt)); retryErr != nil {
|
if retryErr := waitForRetry(ctx, retryDelay(attempt)); retryErr != nil {
|
||||||
return retryErr
|
return retryErr
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
@@ -114,7 +115,13 @@ func GetRenderer() *Renderer {
|
|||||||
return 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]
|
tmpl, ok := r.templates[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("template %s not found", name)
|
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)
|
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]
|
tmpl, ok := r.templates[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("template %s not found", name)
|
return fmt.Errorf("template %s not found", name)
|
||||||
|
|||||||
Reference in New Issue
Block a user