feat(handler,compression): 使用 mimeutil 统一 Content-Type 检测

- 替换 mime.TypeByExtension 为 mimeutil.DetectContentType
- 为 sendfile 大文件路径添加 Content-Type 设置
- 为预压缩文件添加原始 Content-Type
- 添加大文件 Content-Type 测试验证

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-10 16:55:37 +08:00
parent fdab778896
commit 93e3ee0b14
3 changed files with 58 additions and 3 deletions

View File

@ -19,11 +19,12 @@
package handler
import (
"mime"
"os"
"path/filepath"
"strings"
"rua.plus/lolly/internal/mimeutil"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/cache"
"rua.plus/lolly/internal/middleware/compression"
@ -462,7 +463,7 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
if entry.ModTime.Equal(info.ModTime()) {
// 缓存命中且文件未修改
ctx.Response.SetBody(entry.Data)
ctx.Response.Header.SetContentType(mime.TypeByExtension(filepath.Ext(filePath)))
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
return
}
// 文件已修改,删除旧缓存
@ -472,6 +473,9 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
// 大文件使用零拷贝传输
if h.useSendfile && info.Size() >= MinSendfileSize {
// 设置 Content-Type (sendfile 不会自动设置)
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
file, err := os.Open(filePath)
if err == nil {
defer func() { _ = file.Close() }()
@ -495,5 +499,5 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf
}
ctx.Response.SetBody(data)
ctx.Response.Header.SetContentType(mime.TypeByExtension(filepath.Ext(filePath)))
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
}

View File

@ -1402,3 +1402,49 @@ func TestStaticHandler_TryFilesEdgeCases(t *testing.T) {
})
}
}
// TestStaticHandler_LargeFileContentType 测试大文件 sendfile 路径的 Content-Type
func TestStaticHandler_LargeFileContentType(t *testing.T) {
tmpDir := t.TempDir()
// 创建大于 8KB 的文件
largeContent := make([]byte, 16*1024)
for i := range largeContent {
largeContent[i] = byte(i % 256)
}
tests := []struct {
ext string
expected string
}{
{".css", "text/css; charset=utf-8"},
{".js", "text/javascript; charset=utf-8"},
{".webmanifest", "application/manifest+json"},
{".webm", "video/webm"},
{".otf", "font/otf"},
}
for _, tc := range tests {
t.Run(tc.ext, func(t *testing.T) {
filePath := filepath.Join(tmpDir, "large"+tc.ext)
if err := os.WriteFile(filePath, largeContent, 0644); err != nil {
t.Fatalf("创建文件失败: %v", err)
}
handler := NewStaticHandler(tmpDir, "/", nil, true) // 启用 sendfile
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/large" + tc.ext)
handler.Handle(ctx)
if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
}
ct := string(ctx.Response.Header.ContentType())
if ct != tc.expected {
t.Errorf("Content-Type = %q, want %q", ct, tc.expected)
}
})
}
}

View File

@ -20,6 +20,7 @@ import (
"strings"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/mimeutil"
)
// GzipStatic 预压缩文件支持。
@ -112,6 +113,10 @@ func (g *GzipStatic) ServeFile(ctx *fasthttp.RequestCtx, filePath string) bool {
ctx.Response.Header.Set("Content-Encoding", "gzip")
}
ctx.Response.Header.Set("Vary", "Accept-Encoding")
// 设置原始文件的 Content-Type
// filePath 是原始文件路径 (如 "test.js"),直接使用即可
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
fasthttp.ServeFile(ctx, fullPath)
return true
}