feat: Add request body size limit and enhance error logging

This commit introduces two main improvements to the Touka web framework:

1.  **Configurable Request Body Size Limit:**
    - Added `MaxRequestBodySize int64` to `touka.Engine` (default 10MB).
    - You can customize this via `engine.SetMaxRequestBodySize()`.
    - The context methods `GetReqBodyFull()`, `GetReqBodyBuffer()`, and `ShouldBindJSON()` now adhere to this limit. They check `Content-Length` upfront and use `http.MaxBytesReader` to prevent reading excessively large request bodies into memory, enhancing protection against potential DoS attacks or high memory usage.
    - Added comprehensive unit tests in `context_test.go` for this feature, covering scenarios where the limit is active, disabled, and exceeded.

2.  **Enhanced Error Logging in Default Handler:**
    - The `defaultErrorHandle` in `engine.go` now logs not only the primary error passed to it but also any additional errors collected in `Context.Errors` (via `c.AddError()`).
    - This provides more comprehensive diagnostic information in the logs without altering the JSON error response structure sent to the client, ensuring backward compatibility.

These changes aim to improve the framework's robustness, memory safety, and debuggability.
This commit is contained in:
google-labs-jules[bot] 2025-06-20 06:35:01 +00:00
parent 543b3165ca
commit 82099e26ee
3 changed files with 336 additions and 15 deletions

View file

@ -330,14 +330,27 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
if c.Request.Body == nil {
return errors.New("request body is empty")
}
/*
decoder := json.NewDecoder(c.Request.Body)
if err := decoder.Decode(obj); err != nil {
return fmt.Errorf("json binding error: %w", err)
// defer c.Request.Body.Close() // 通常由调用方或中间件确保关闭,但如果这里是唯一消耗点,可以考虑
var reader io.Reader = c.Request.Body
if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
return fmt.Errorf("request body size (%d bytes) exceeds configured limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
}
*/
err := json.UnmarshalRead(c.Request.Body, obj)
// 注意http.MaxBytesReader(nil, ...) 中的 nil ResponseWriter 参数意味着当超出限制时,
// MaxBytesReader 会直接返回错误,而不会尝试写入 HTTP 错误响应。这对于 API 来说是合适的。
reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
}
err := json.UnmarshalRead(reader, obj)
if err != nil {
// 检查错误类型是否为 http.MaxBytesError
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
return fmt.Errorf("request body size exceeds configured limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
}
// 检查是否是 json 相关的错误,可能需要更细致的错误处理
// 例如json.SyntaxError, json.UnmarshalTypeError 等
return fmt.Errorf("json binding error: %w", err)
}
return nil
@ -441,13 +454,31 @@ func (c *Context) GetReqBody() io.ReadCloser {
// 注意:请求体只能读取一次
func (c *Context) GetReqBodyFull() ([]byte, error) {
if c.Request.Body == nil {
return nil, nil
return nil, nil // 或者返回一个错误: errors.New("request body is nil")
}
defer c.Request.Body.Close() // 确保请求体被关闭
data, err := io.ReadAll(c.Request.Body)
var reader io.Reader = c.Request.Body
if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
err := fmt.Errorf("request body size (%d bytes) exceeds configured limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
c.AddError(err)
return nil, err
}
reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
}
data, err := io.ReadAll(reader)
if err != nil {
c.AddError(fmt.Errorf("failed to read request body: %w", err))
return nil, fmt.Errorf("failed to read request body: %w", err)
// 检查错误类型是否为 http.MaxBytesError如果是则表示超出了限制
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
err = fmt.Errorf("request body size exceeds configured limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
} else {
err = fmt.Errorf("failed to read request body: %w", err)
}
c.AddError(err)
return nil, err
}
return data, nil
}
@ -455,13 +486,30 @@ func (c *Context) GetReqBodyFull() ([]byte, error) {
// 类似 GetReqBodyFull, 返回 *bytes.Buffer
func (c *Context) GetReqBodyBuffer() (*bytes.Buffer, error) {
if c.Request.Body == nil {
return nil, nil
return nil, nil // 或者返回一个错误: errors.New("request body is nil")
}
defer c.Request.Body.Close() // 确保请求体被关闭
data, err := io.ReadAll(c.Request.Body)
var reader io.Reader = c.Request.Body
if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
err := fmt.Errorf("request body size (%d bytes) exceeds configured limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
c.AddError(err)
return nil, err
}
reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
}
data, err := io.ReadAll(reader)
if err != nil {
c.AddError(fmt.Errorf("failed to read request body: %w", err))
return nil, fmt.Errorf("failed to read request body: %w", err)
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
err = fmt.Errorf("request body size exceeds configured limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
} else {
err = fmt.Errorf("failed to read request body: %w", err)
}
c.AddError(err)
return nil, err
}
return bytes.NewBuffer(data), nil
}