package handler import ( "context" "errors" "io" "net" "net/http" "strings" "syscall" "go.uber.org/zap" ) // logRequestBodyReadFailure records a bounded, payload-free reason for a body // read failure. Clients continue to receive the stable generic error message; // operators get enough information to distinguish compression failures from a // disconnected/truncated upload without logging request content. func logRequestBodyReadFailure(reqLog *zap.Logger, req *http.Request, err error) { if reqLog == nil || err == nil { return } contentLength := int64(-1) contentEncoding := "identity" if req != nil { contentLength = req.ContentLength contentEncoding = requestContentEncodingCategory(req.Header.Get("Content-Encoding")) } reqLog.Warn("read request body failed", zap.String("error_kind", requestBodyReadErrorKind(err)), zap.String("content_encoding", contentEncoding), zap.Int64("content_length", contentLength), ) } func requestContentEncodingCategory(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "", "identity": return "identity" case "gzip", "x-gzip": return "gzip" case "zstd": return "zstd" case "deflate": return "deflate" default: return "other" } } func requestBodyReadErrorKind(err error) string { if err == nil { return "none" } var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { return "max_bytes" } lower := strings.ToLower(err.Error()) if strings.Contains(lower, "decode content-encoding") { if strings.Contains(lower, "unsupported content-encoding") { return "unsupported_content_encoding" } return "decode_content_encoding" } if errors.Is(err, context.Canceled) || errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) { return "client_disconnect" } if errors.Is(err, io.ErrUnexpectedEOF) { return "truncated_body" } var netErr net.Error if errors.As(err, &netErr) { return "transport" } return "io_read" }