Add panic recovery and request size limit
This commit is contained in:
91
internal/server/middleware.go
Normal file
91
internal/server/middleware.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/ajac-zero/latticelm/internal/logger"
|
||||
)
|
||||
|
||||
// MaxRequestBodyBytes is the maximum size allowed for request bodies (10MB)
|
||||
const MaxRequestBodyBytes = 10 * 1024 * 1024
|
||||
|
||||
// PanicRecoveryMiddleware recovers from panics in HTTP handlers and logs them
|
||||
// instead of crashing the server. Returns 500 Internal Server Error to the client.
|
||||
func PanicRecoveryMiddleware(next http.Handler, log *slog.Logger) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// Capture stack trace
|
||||
stack := debug.Stack()
|
||||
|
||||
// Log the panic with full context
|
||||
log.ErrorContext(r.Context(), "panic recovered in HTTP handler",
|
||||
logger.LogAttrsWithTrace(r.Context(),
|
||||
slog.String("request_id", logger.FromContext(r.Context())),
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("remote_addr", r.RemoteAddr),
|
||||
slog.Any("panic", err),
|
||||
slog.String("stack", string(stack)),
|
||||
)...,
|
||||
)
|
||||
|
||||
// Return 500 to client
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RequestSizeLimitMiddleware enforces a maximum request body size to prevent
|
||||
// DoS attacks via oversized payloads. Requests exceeding the limit receive 413.
|
||||
func RequestSizeLimitMiddleware(next http.Handler, maxBytes int64) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only limit body size for requests that have a body
|
||||
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch {
|
||||
// Wrap the request body with a size limiter
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorRecoveryMiddleware catches errors from MaxBytesReader and converts them
|
||||
// to proper HTTP error responses. This should be placed after RequestSizeLimitMiddleware
|
||||
// in the middleware chain.
|
||||
func ErrorRecoveryMiddleware(next http.Handler, log *slog.Logger) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
// Check if the request body exceeded the size limit
|
||||
// MaxBytesReader sets an error that we can detect on the next read attempt
|
||||
// But we need to handle the error when it actually occurs during JSON decoding
|
||||
// The JSON decoder will return the error, so we don't need special handling here
|
||||
// This middleware is more for future extensibility
|
||||
})
|
||||
}
|
||||
|
||||
// WriteJSONError is a helper function to safely write JSON error responses,
|
||||
// handling any encoding errors that might occur.
|
||||
func WriteJSONError(w http.ResponseWriter, log *slog.Logger, message string, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
// Use fmt.Fprintf to write the error response
|
||||
// This is safer than json.Encoder as we control the format
|
||||
_, err := fmt.Fprintf(w, `{"error":{"message":"%s"}}`, message)
|
||||
if err != nil {
|
||||
// If we can't even write the error response, log it
|
||||
log.Error("failed to write error response",
|
||||
slog.String("original_message", message),
|
||||
slog.Int("status_code", statusCode),
|
||||
slog.String("write_error", err.Error()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user