mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-10-28 16:46:57 +08:00
GLOBAL_WEB_RATE_LIMIT (120 requests per 180 s per IP) counted every file served from the frontend build. Since provider icons became lazy chunks, one cold session sends about 90 static requests, so a second user behind the same IP gets 429 on route chunks and the page fails to load. Files that exist in the build now count against a separate static limiter (GLOBAL_STATIC_RATE_LIMIT_ENABLE, GLOBAL_STATIC_RATE_LIMIT, GLOBAL_STATIC_RATE_LIMIT_DURATION), disabled by default. Pages and unknown paths still count against the web limit. The gzip middleware also recompressed every static response, about 120 ms of CPU per request for the 4.8 MB main bundle. Build files are embedded and never change, so each one is now served from a gzip encoding computed once per process. Pages and unknown API paths still go through the gzip middleware.
110 lines
2.9 KiB
Go
110 lines
2.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"errors"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
ginGzip "github.com/gin-contrib/gzip"
|
|
"github.com/gin-contrib/static"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// precompressedFile is the gzip encoding of one frontend build file.
|
|
type precompressedFile struct {
|
|
once sync.Once
|
|
body []byte
|
|
contentType string
|
|
err error
|
|
}
|
|
|
|
// ServeFrontendFiles serves files from the embedded frontend build. The build
|
|
// never changes while the process runs, so each file is gzip-compressed once
|
|
// and the bytes are reused, instead of recompressing multi-megabyte chunks on
|
|
// every request. The cache holds at most one entry per build file. Clients
|
|
// without gzip, the image types the gzip middleware skips, directories, and
|
|
// the index.html redirect keep using http.FileServer.
|
|
func ServeFrontendFiles(frontendFS static.ServeFileSystem) gin.HandlerFunc {
|
|
fileServer := http.FileServer(frontendFS)
|
|
var cache sync.Map // URL path -> *precompressedFile
|
|
return func(c *gin.Context) {
|
|
urlPath := c.Request.URL.Path
|
|
if !frontendFS.Exists("/", urlPath) {
|
|
return
|
|
}
|
|
c.Abort()
|
|
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") ||
|
|
ginGzip.DefaultExcludedExtentions.Contains(path.Ext(urlPath)) ||
|
|
strings.HasSuffix(urlPath, "/index.html") {
|
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
|
return
|
|
}
|
|
|
|
value, ok := cache.Load(urlPath)
|
|
if !ok {
|
|
value, _ = cache.LoadOrStore(urlPath, &precompressedFile{})
|
|
}
|
|
file := value.(*precompressedFile)
|
|
file.once.Do(func() {
|
|
f, err := frontendFS.Open(urlPath)
|
|
if err != nil {
|
|
file.err = err
|
|
return
|
|
}
|
|
defer f.Close()
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
file.err = err
|
|
return
|
|
}
|
|
if info.IsDir() {
|
|
file.err = errors.New("directory")
|
|
return
|
|
}
|
|
raw, err := io.ReadAll(f)
|
|
if err != nil {
|
|
file.err = err
|
|
return
|
|
}
|
|
var compressed bytes.Buffer
|
|
writer := gzip.NewWriter(&compressed)
|
|
if _, err := writer.Write(raw); err != nil {
|
|
file.err = err
|
|
return
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
file.err = err
|
|
return
|
|
}
|
|
// Match http.FileServer: extension first, then content sniffing.
|
|
file.contentType = mime.TypeByExtension(path.Ext(urlPath))
|
|
if file.contentType == "" {
|
|
file.contentType = http.DetectContentType(raw)
|
|
}
|
|
file.body = bytes.Clone(compressed.Bytes())
|
|
})
|
|
if file.err != nil {
|
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
|
return
|
|
}
|
|
|
|
// Range requests get the whole body; a server may ignore Range.
|
|
header := c.Writer.Header()
|
|
header.Set("Content-Encoding", "gzip")
|
|
header.Set("Vary", "Accept-Encoding")
|
|
header.Set("Content-Type", file.contentType)
|
|
header.Set("Content-Length", strconv.Itoa(len(file.body)))
|
|
c.Writer.WriteHeader(http.StatusOK)
|
|
if c.Request.Method != http.MethodHead {
|
|
_, _ = c.Writer.Write(file.body)
|
|
}
|
|
}
|
|
}
|