Skip to content

Commit b3d0978

Browse files
authoredAug 19, 2024··
Fixes 4432: log request body for server errors (#773)
* Fixes 4432: log request body for server errors * Fixes 4432: buffered reader, method filter * Fixes 4432: cleanup
1 parent c7971cc commit b3d0978

File tree

4 files changed

+222
-1
lines changed

4 files changed

+222
-1
lines changed
 
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package middleware
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"io"
7+
"net/http"
8+
9+
ce "github.com/content-services/content-sources-backend/pkg/errors"
10+
"github.com/labstack/echo/v4"
11+
)
12+
13+
const BodyDumpLimit = 1000
14+
const BodyStoreKey = "body_backup"
15+
16+
func LogServerErrorRequest(next echo.HandlerFunc) echo.HandlerFunc {
17+
return func(c echo.Context) (err error) {
18+
if c.Get(BodyStoreKey) == nil && isBodiedMethod(c.Request().Method) {
19+
storeRequestBody(c)
20+
}
21+
if err = next(c); err != nil {
22+
if containsServerError(err) && isBodiedMethod(c.Request().Method) {
23+
logRequestBody(c)
24+
}
25+
return err
26+
}
27+
return nil
28+
}
29+
}
30+
31+
func containsServerError(err error) bool {
32+
httpError := new(ce.ErrorResponse)
33+
if errors.As(err, httpError) {
34+
for _, e := range httpError.Errors {
35+
if e.Status >= http.StatusInternalServerError {
36+
return true
37+
}
38+
}
39+
}
40+
return false
41+
}
42+
43+
func logRequestBody(c echo.Context) {
44+
if body := c.Get(BodyStoreKey); body != nil {
45+
storedBodyBytes, ok := body.([]byte)
46+
if !ok {
47+
c.Logger().Error("Error reading request body")
48+
}
49+
c.Logger().Errorf("Request body: %v", string(storedBodyBytes))
50+
}
51+
}
52+
53+
func storeRequestBody(c echo.Context) {
54+
limit := BodyDumpLimit
55+
buffered := BufferedReadCloser{bufio.NewReader(c.Request().Body), c.Request().Body}
56+
57+
bytes, err := buffered.Peek(BodyDumpLimit)
58+
if errors.Is(err, io.EOF) {
59+
limit = len(bytes)
60+
err = nil
61+
}
62+
if errors.Is(err, bufio.ErrBufferFull) {
63+
err = nil
64+
}
65+
if err != nil {
66+
c.Logger().Error("Error reading request body")
67+
return
68+
}
69+
70+
c.Set(BodyStoreKey, bytes[:limit])
71+
c.Request().Body = buffered
72+
}
73+
74+
func isBodiedMethod(method string) bool {
75+
switch method {
76+
case http.MethodGet:
77+
return false
78+
case http.MethodDelete:
79+
return false
80+
default:
81+
return true
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
12+
ce "github.com/content-services/content-sources-backend/pkg/errors"
13+
"github.com/labstack/echo/v4"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
const TestBody = `{"limit": 100, "search": "random", "uuids": ["e88efd75-2b29-4b59-8867-bb60435b3742"]}`
18+
const LargeBody = `{"limit": 100, "search": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Integer pellentesque quam vel velit. Vivamus porttitor turpis ac leo. Ut tempus purus at lorem. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Nulla pulvinar eleifend sem. Proin mattis lacinia justo. Integer malesuada. Etiam sapien elit, consequat eget, tristique non, venenatis quis, ante. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Integer lacinia. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Curabitur bibendum justo non orci. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Etiam sapien elit, consequat eget, tristique non, venenatis quis, ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas sollicitudin. Quisque porta. Nullam sapien sem, ornare ac, nonummy non, lobortis a enim. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas sollicitudin. Quisque porta. Nullam sapien sem, ornare ac, nonummy non, lobortis a enim.", "uuids": ["e88efd75-2b29-4b59-8867-bb60435b3742"]}`
19+
20+
func TestReadBodyStoreAndRestore(t *testing.T) {
21+
e := echo.New()
22+
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(TestBody))
23+
rec := httptest.NewRecorder()
24+
c := e.NewContext(req, rec)
25+
h := LogServerErrorRequest(func(c echo.Context) error {
26+
return c.JSON(http.StatusInternalServerError, map[string]string{
27+
"status": fmt.Sprintf("%d", http.StatusInternalServerError),
28+
"title": "testing error",
29+
})
30+
})
31+
32+
_ = h(c)
33+
34+
var reqBody []byte
35+
if c.Request().Body != nil {
36+
reqBody, _ = io.ReadAll(c.Request().Body)
37+
}
38+
39+
storedBody := c.Get(BodyStoreKey)
40+
storedBodyBytes, ok := storedBody.([]byte)
41+
if !ok {
42+
assert.Fail(t, "Failed type assertion of stored body.")
43+
}
44+
45+
assert.Equal(t, http.StatusInternalServerError, rec.Code)
46+
assert.Equal(t, TestBody, string(storedBodyBytes))
47+
assert.Equal(t, TestBody, string(reqBody))
48+
}
49+
50+
func TestLargeBodyCutoff(t *testing.T) {
51+
e := echo.New()
52+
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(LargeBody))
53+
rec := httptest.NewRecorder()
54+
c := e.NewContext(req, rec)
55+
h := LogServerErrorRequest(func(c echo.Context) error {
56+
return c.JSON(http.StatusInternalServerError, map[string]string{
57+
"status": fmt.Sprintf("%d", http.StatusInternalServerError),
58+
"title": "testing error",
59+
})
60+
})
61+
62+
_ = h(c)
63+
64+
var reqBody []byte
65+
if c.Request().Body != nil {
66+
reqBody, _ = io.ReadAll(c.Request().Body)
67+
}
68+
69+
storedBody := c.Get(BodyStoreKey)
70+
storedBodyBytes, ok := storedBody.([]byte)
71+
if !ok {
72+
assert.Fail(t, "Failed type assertion of stored body.")
73+
}
74+
75+
assert.Equal(t, http.StatusInternalServerError, rec.Code)
76+
assert.Equal(t, LargeBody[:1000], string(storedBodyBytes))
77+
assert.Equal(t, LargeBody, string(reqBody))
78+
}
79+
80+
func TestRequestBodyLogging(t *testing.T) {
81+
e := echo.New()
82+
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(TestBody))
83+
rec := httptest.NewRecorder()
84+
c := e.NewContext(req, rec)
85+
c.Set(BodyStoreKey, []byte(TestBody))
86+
buf := new(bytes.Buffer)
87+
c.Logger().SetOutput(buf)
88+
h := LogServerErrorRequest(func(c echo.Context) error {
89+
return ce.NewErrorResponse(http.StatusInternalServerError, "Test Error", "5xx testing error")
90+
})
91+
92+
_ = h(c)
93+
94+
log := make(map[string]string)
95+
_ = json.Unmarshal(buf.Bytes(), &log)
96+
97+
assert.True(t, bytes.Contains(buf.Bytes(), []byte("\"message\":\"Request body: {\\\"limit\\\": 100, \\\"search\\\": \\\"random\\\", \\\"uuids\\\": [\\\"e88efd75-2b29-4b59-8867-bb60435b3742\\\"]}\"}")))
98+
assert.Equal(t, log["message"], fmt.Sprintf("Request body: %s", TestBody))
99+
assert.Equal(t, log["level"], "ERROR")
100+
}
101+
102+
func TestMethodWithoutBody(t *testing.T) {
103+
e := echo.New()
104+
req := httptest.NewRequest(http.MethodGet, "/", nil)
105+
rec := httptest.NewRecorder()
106+
c := e.NewContext(req, rec)
107+
buf := new(bytes.Buffer)
108+
c.Logger().SetOutput(buf)
109+
h := LogServerErrorRequest(func(c echo.Context) error {
110+
return ce.NewErrorResponse(http.StatusInternalServerError, "Test Error", "5xx testing error")
111+
})
112+
113+
_ = h(c)
114+
115+
log := make(map[string]string)
116+
_ = json.Unmarshal(buf.Bytes(), &log)
117+
118+
assert.Equal(t, []byte(nil), buf.Bytes())
119+
}

‎pkg/middleware/utils.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package middleware
22

3-
import "github.com/labstack/echo/v4"
3+
import (
4+
"bufio"
5+
"io"
6+
7+
"github.com/labstack/echo/v4"
8+
)
49

510
// See: https://github.com/labstack/echo/pull/1502/files
611
// This method exist for v5 echo framework
@@ -13,3 +18,16 @@ func MatchedRoute(ctx echo.Context) string {
1318
}
1419
return ""
1520
}
21+
22+
type BufferedReadCloser struct {
23+
*bufio.Reader
24+
io.ReadCloser
25+
}
26+
27+
func (rw BufferedReadCloser) Close() error {
28+
return rw.ReadCloser.Close()
29+
}
30+
31+
func (rw BufferedReadCloser) Read(p []byte) (int, error) {
32+
return rw.Reader.Read(p)
33+
}

‎pkg/router/route.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func ConfigureEcho(allRoutes bool) *echo.Echo {
3434
}))
3535
e.Use(middleware.ExtractStatus) // Must be after lecho
3636
e.Use(middleware.EnforceJSONContentType)
37+
e.Use(middleware.LogServerErrorRequest)
3738

3839
// Add routes
3940
handler.RegisterPing(e)

0 commit comments

Comments
 (0)
Please sign in to comment.