// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package route import ( "bytes" "io" "net/http" "net/http/httptest" "testing" "github.com/flamego/flamego" "github.com/tamakyi/TamaBox/internal/form" ) // TestRequestBodyLimitEnforced 是 P1-1(请求体 12MiB 上限必须在任何解析之前全局生效) // 的回归测试。 // // 背景:此前 MaxBytesReader 只装在 form.Bind(路由级),但 Contexter 组级 CSRF 校验 // (无 X-CSRF-Token 头时经 FormValue("_csrf") 惰性解析整包 body)先于 Bind 执行, // 超大 multipart 会在路由级限制生效前无上限落盘临时文件耗尽磁盘;chat /send、 // vote /submit 等不走 Bind 的入口更是完全没有上限。修复为全局中间件 // requestBodyLimitMiddleware(挂载于 f.Use 链,先于所有 Group/CSRF)。 // // 本测试验证三件事(用内存 flamego 实例,无需 DB/Redis): // 1. 合法请求(≤8MiB 单图上限 + 表单开销)不被误伤 → 200; // 2. Content-Length 明确超限 → 中间件直接 413 快速失败,不进入 handler; // 3. chunked/未知长度超限(绕过 Content-Length 预检)→ MaxBytesReader 在读超限时报错, // 服务端拒绝,而非默默读入/落盘后 200。 func TestRequestBodyLimitEnforced(t *testing.T) { f := flamego.New() // 与 internal/route/route.go 一致:全局 Use 挂载先于任何路由/组 handler。 f.Use(requestBodyLimitMiddleware()) f.Post("/echo", func(ctx flamego.Context) { body, err := io.ReadAll(ctx.Request().Request.Body) if err != nil { // MaxBytesReader 超限读报错:服务端拒绝,不得返回 200。 ctx.ResponseWriter().WriteHeader(http.StatusRequestEntityTooLarge) return } ctx.ResponseWriter().WriteHeader(http.StatusOK) _, _ = ctx.ResponseWriter().Write(body) }) ts := httptest.NewServer(f) defer ts.Close() post := func(payload []byte, chunked bool) int { req, _ := http.NewRequest(http.MethodPost, ts.URL+"/echo", bytes.NewReader(payload)) req.ContentLength = int64(len(payload)) if chunked { req.ContentLength = -1 // 强制 chunked,绕过 Content-Length 预检 } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("POST failed: %v", err) } defer func() { _ = resp.Body.Close() }() _, _ = io.Copy(io.Discard, resp.Body) return resp.StatusCode } // 1. 合法:8MiB(≈单图上限 storage.MaxUploadBytes + 表单开销)→ 必须放行。 legal := bytes.Repeat([]byte{'a'}, 8<<20) if code := post(legal, false); code != http.StatusOK { t.Errorf("合法 8MiB 请求被拒绝:got %d, want 200", code) } // 2. Content-Length 明确超限 → 413(中间件预检,不进入 handler)。 huge := bytes.Repeat([]byte{'b'}, form.MaxRequestBodySize+1) if code := post(huge, false); code != http.StatusRequestEntityTooLarge { t.Errorf("Content-Length 超限请求未被 413:got %d, want 413", code) } // 3. chunked 超限(无 Content-Length,必须靠 MaxBytesReader 惰性封顶)→ 413。 if code := post(huge, true); code != http.StatusRequestEntityTooLarge { t.Errorf("chunked 超限请求未被拒绝:got %d, want 413", code) } }