🏡 index : quine-global/quintodrome.git

package server

import (
	"bytes"
	"context"
	"errors"
	"maps"
	"net/http"
	"sync"
	"time"

	"github.com/go-chi/chi/v5/middleware"
	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/log"
)

var (
	ErrThrottleCapacityExceeded = errors.New("throttle: capacity exceeded")
	ErrThrottleTimeout          = errors.New("throttle: backlog timeout")
)

type requestThrottle struct {
	tokens         chan struct{}
	backlogTokens  chan struct{}
	backlogTimeout time.Duration
}

// ThrottleBacklog creates a Chi-compatible middleware that limits concurrent
// request processing. Unlike Chi's ThrottleBacklog, it buffers the handler's
// response while holding the token, releases it, then flushes the buffer to
// the client with a write deadline. This prevents slow clients from holding
// throttle capacity.
//
// Because it buffers the entire response in memory, this middleware should only
// be used for endpoints that return small responses (e.g., artwork images). Do
// not use it for audio streaming or download endpoints.
func ThrottleBacklog(limit, backlogLimit int, backlogTimeout time.Duration) func(http.Handler) http.Handler {
	if limit <= 0 {
		return func(next http.Handler) http.Handler { return next }
	}
	if !conf.Server.DevArtworkThrottleBuffered {
		return middleware.ThrottleBacklog(limit, backlogLimit, backlogTimeout)
	}
	t := &requestThrottle{
		tokens:         make(chan struct{}, limit),
		backlogTokens:  make(chan struct{}, limit+backlogLimit),
		backlogTimeout: backlogTimeout,
	}
	for range limit {
		t.tokens <- struct{}{}
	}
	for range limit + backlogLimit {
		t.backlogTokens <- struct{}{}
	}
	return t.handler
}

func (t *requestThrottle) handler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		release, err := t.acquire(ctx)
		if err != nil {
			switch {
			case errors.Is(err, ErrThrottleCapacityExceeded):
				log.Warn(ctx, "Request throttle capacity exceeded", "path", r.URL.Path)
			case errors.Is(err, ErrThrottleTimeout):
				log.Warn(ctx, "Request throttle backlog timeout", "path", r.URL.Path)
			}
			http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
			return
		}

		buf := &bufferedResponseWriter{header: make(http.Header)}
		func() {
			defer release()
			next.ServeHTTP(buf, r)
		}()

		maps.Copy(w.Header(), buf.header)
		if buf.code > 0 {
			w.WriteHeader(buf.code)
		}
		if _, err := w.Write(buf.body.Bytes()); err != nil {
			log.Warn(ctx, "Error writing throttled response", err)
		}
	})
}

func (t *requestThrottle) acquire(ctx context.Context) (release func(), err error) {
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-t.backlogTokens:
	default:
		return nil, ErrThrottleCapacityExceeded
	}

	select {
	case <-t.tokens:
		return t.releaseFunc(), nil
	default:
	}

	timer := time.NewTimer(t.backlogTimeout)
	select {
	case <-timer.C:
		t.backlogTokens <- struct{}{}
		return nil, ErrThrottleTimeout
	case <-ctx.Done():
		timer.Stop()
		t.backlogTokens <- struct{}{}
		return nil, ctx.Err()
	case <-t.tokens:
		timer.Stop()
		return t.releaseFunc(), nil
	}
}

func (t *requestThrottle) releaseFunc() func() {
	var once sync.Once
	return func() {
		once.Do(func() {
			t.tokens <- struct{}{}
			t.backlogTokens <- struct{}{}
		})
	}
}

type bufferedResponseWriter struct {
	header http.Header
	body   bytes.Buffer
	code   int
}

func (w *bufferedResponseWriter) Header() http.Header {
	return w.header
}

func (w *bufferedResponseWriter) Write(b []byte) (int, error) {
	return w.body.Write(b)
}

func (w *bufferedResponseWriter) WriteHeader(code int) {
	if w.code != 0 {
		return
	}
	w.code = code
}