Skip to content

fix: do not consume request body unless explicitly requested #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,24 @@ func main() {
LogResponseBody: isDebugHeaderSet,

// Log all requests with invalid payload as curl command.
LogExtraAttrs: func(req *http.Request, reqBody string, respStatus int) []slog.Attr {
if respStatus == 400 || respStatus == 422 {
req.Header.Del("Authorization")
return []slog.Attr{slog.String("curl", httplog.CURL(req, reqBody))}
}
return nil
LogAdditionalAttrs: &httplog.LogAdditionalAttrsOptions{
IncludeRequestBody: isAdditionalAttrsDebugHeaderSet,
AdditionalAttrs: func(ld *httplog.LogDetails) []slog.Attr {
if ld.ResponseStatus == 400 || ld.ResponseStatus == 422 {
ld.Request.Header.Del("Authorization")
return []slog.Attr{slog.String("curl", httplog.CURL(ld.Request, ld.RequestBody))}
}
return nil
},
},
// Deprecated: LogExtraAttrs is deprecated, use LogAdditionalAttrs instead.
// LogExtraAttrs: func(req *http.Request, reqBody string, respStatus int) []slog.Attr {
// if respStatus == 400 || respStatus == 422 {
// req.Header.Del("Authorization")
// return []slog.Attr{slog.String("curl", httplog.CURL(req, reqBody))}
// }
// return nil
// },
}))

// Set request log attribute from within middleware.
Expand Down Expand Up @@ -190,6 +201,7 @@ func main() {
fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"data": "valid payload"}'`)
fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"data": "valid payload"}' -H "Debug: reveal-body-logs"`)
fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"xx": "invalid payload"}'`)
fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"xx": "invalid payload"}' -H "AdditionalAttrsDebug: reveal-body-logs"`)
fmt.Println()

if err := http.ListenAndServe("localhost:8000", r); err != http.ErrAbortHandler {
Expand Down Expand Up @@ -217,3 +229,7 @@ func logHandler(isLocalhost bool, handlerOpts *slog.HandlerOptions) slog.Handler
func isDebugHeaderSet(r *http.Request) bool {
return r.Header.Get("Debug") == "reveal-body-logs"
}

func isAdditionalAttrsDebugHeaderSet(r *http.Request) bool {
return r.Header.Get("AdditionalAttrsDebug") == "reveal-body-logs"
}
25 changes: 22 additions & 3 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,19 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand
logReqBody := o.LogRequestBody != nil && o.LogRequestBody(r)
logRespBody := o.LogResponseBody != nil && o.LogResponseBody(r)

var includeAdditionalAttrsReqBody bool
if o.LogAdditionalAttrs != nil {
if o.LogAdditionalAttrs.AdditionalAttrs != nil && o.LogAdditionalAttrs.IncludeRequestBody != nil {
includeAdditionalAttrsReqBody = o.LogAdditionalAttrs.IncludeRequestBody(r)
}
} else if o.LogExtraAttrs != nil {
includeAdditionalAttrsReqBody = true
}
hasReqBody := r.Body != nil && r.Body != http.NoBody
consumeBody := hasReqBody && (logReqBody || includeAdditionalAttrsReqBody)

var reqBody bytes.Buffer
if logReqBody || o.LogExtraAttrs != nil {
if consumeBody {
r.Body = io.NopCloser(io.TeeReader(r.Body, &reqBody))
}

Expand Down Expand Up @@ -142,7 +153,7 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand
logAttrs = appendAttrs(logAttrs, slog.Any(ErrorKey, ErrClientAborted), slog.String(s.ErrorType, "ClientAborted"))
}

if logReqBody || o.LogExtraAttrs != nil {
if consumeBody {
// Ensure the request body is fully read if the underlying HTTP handler didn't do so.
n, _ := io.Copy(io.Discard, r.Body)
if n > 0 {
Expand All @@ -155,7 +166,15 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand
if logRespBody {
logAttrs = appendAttrs(logAttrs, slog.String(s.ResponseBody, logBody(&respBody, ww.Header(), o)))
}
if o.LogExtraAttrs != nil {
if o.LogAdditionalAttrs != nil {
if o.LogAdditionalAttrs.AdditionalAttrs != nil {
logAttrs = appendAttrs(logAttrs, o.LogAdditionalAttrs.AdditionalAttrs(&LogDetails{
Request: r,
RequestBody: reqBody.String(),
ResponseStatus: statusCode,
})...)
}
} else if o.LogExtraAttrs != nil {
logAttrs = appendAttrs(logAttrs, o.LogExtraAttrs(r, reqBody.String(), statusCode)...)
}
logAttrs = appendAttrs(logAttrs, getAttrs(ctx)...)
Expand Down
41 changes: 41 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,15 @@ type Options struct {
// }
//
// WARNING: Be careful not to leak any sensitive information in the logs.
//
// Deprecated: Use LogAdditionalAttrs instead. Will be ignored if LogAdditionalAttrs is set.
LogExtraAttrs func(req *http.Request, reqBody string, respStatus int) []slog.Attr

// LogAdditionalAttrs is an optional way for you to add additional attributes to the
// request log.
//
// WARNING: Be careful not to leak any sensitive information in the logs.
LogAdditionalAttrs *LogAdditionalAttrsOptions
}

var defaultOptions = Options{
Expand All @@ -106,3 +114,36 @@ var defaultOptions = Options{
LogBodyContentTypes: []string{"application/json", "application/xml", "text/plain", "text/csv", "application/x-www-form-urlencoded", ""},
LogBodyMaxLen: 1024,
}

type LogAdditionalAttrsOptions struct {
// IncludeRequestBody is an optional predicate function that controls if LogDetails.RequestBody passed to AdditionalAttrs will contain the full request body.
//
// If the function returns true or if Options.LogRequestBody returns true, the request body will be passed in LogDetails.RequestBody.
// If false or not set and Options.LogRequestBody is not set or returns false, an empty string will be passed in LogDetails.RequestBody.
//
// WARNING: Do not leak any request bodies with sensitive information.
IncludeRequestBody func(req *http.Request) bool
// AdditionalAttrs is an optional way for you to add additional attributes to the
// request log.
//
// Example:
//
// // Log all requests with invalid payload as curl command.
// func(ld *LogDetails) []slog.Attr {
// if ld.ResponseStatus == 400 || ld.ResponseStatus == 422 {
// ld.Request.Header.Del("Authorization")
// return []slog.Attr{slog.String("curl", httplog.CURL(req, reqBody))}
// }
// return nil
// }
//
// WARNING: Be careful not to leak any sensitive information in the logs.
AdditionalAttrs func(ld *LogDetails) []slog.Attr
}

type LogDetails struct {
Request *http.Request
// Contains the request body if either Options.LogRequestBody or LogAdditionalAttrsOptions.IncludeRequestBody is true, otherwise it is empty.
RequestBody string
ResponseStatus int
}