feat: request deduplication / debouncing

Kobo eReaders have a buggy browser that makes 2 requests for
the same HTTP resource when you click a link.

This change ensures that requests within a certain time frame
from the same IP, for the same path  / query params will only
be executed a single time.

We record the http request response and replay it for the second
request. If we get 2 simultaneous requests, we use the
sync/singleflight library to ensure only the first request is actually
processed. The second waits for the shared result of the first.

This probably adds latency since some requests are blocked while
we determine if we already have a cache entry, but for a simple
service like this I don't think it matters.
This commit is contained in:
Evan Buss
2024-08-18 18:37:42 +00:00
parent 5d45afd419
commit 0f0540549d
6 changed files with 248 additions and 2 deletions

View File

@@ -19,6 +19,7 @@ import (
"github.com/evan-buss/opds-proxy/convert"
"github.com/evan-buss/opds-proxy/html"
"github.com/evan-buss/opds-proxy/internal/debounce"
"github.com/evan-buss/opds-proxy/opds"
"github.com/google/uuid"
"github.com/gorilla/securecookie"
@@ -72,9 +73,15 @@ func NewServer(config *ProxyConfig) (*Server, error) {
s := securecookie.New(hashKey, blockKey)
// Kobo issues 2 requests for each clicked link. This middleware ensures
// we only process the first request and provide the same response for the second.
// This becomes more important when the requests aren't idempotent, such as triggering
// a download.
debounceMiddleware := debounce.NewDebounceMiddleware(time.Millisecond * 100)
router := http.NewServeMux()
router.Handle("GET /{$}", requestMiddleware(handleHome(config.Feeds)))
router.Handle("GET /feed", requestMiddleware(handleFeed("tmp/", config.Feeds, s)))
router.Handle("GET /feed", requestMiddleware(debounceMiddleware(handleFeed("tmp/", config.Feeds, s))))
router.Handle("/auth", requestMiddleware(handleAuth(s)))
router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles())))
@@ -120,7 +127,12 @@ func requestMiddleware(next http.Handler) http.Handler {
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
log.Info("Request Completed", slog.String("duration", time.Since(start).String()))
log.Info("Request Completed",
slog.String("duration", time.Since(start).String()),
slog.Bool("debounce", w.Header().Get("X-Debounce") == "true"),
slog.Bool("shared", w.Header().Get("X-Shared") == "true"),
)
})
}
@@ -208,6 +220,7 @@ func handleFeed(outputDir string, feeds []FeedConfig, s *securecookie.SecureCook
handleError(r, w, "Failed to render feed", err)
return
}
return
}
mutex.Lock()