-
Notifications
You must be signed in to change notification settings - Fork 8
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
Replace API Debug Middleware #631
Open
DanielCosme
wants to merge
1
commit into
main
Choose a base branch
from
fix-api-debug
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package api | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/http" | ||
"sort" | ||
"strings" | ||
|
||
goahttp "goa.design/goa/v3/http" | ||
"goa.design/goa/v3/middleware" | ||
) | ||
|
||
// Debug returns a debug middleware which prints detailed information about | ||
// incoming requests and outgoing responses including all headers, parameters | ||
// and bodies. | ||
func Debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler { | ||
return func(h http.Handler) http.Handler { | ||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||
buf := &bytes.Buffer{} | ||
// Request ID | ||
reqID := r.Context().Value(middleware.RequestIDKey) | ||
if reqID == nil { | ||
reqID = shortID() | ||
} | ||
|
||
// Request URL | ||
buf.WriteString(fmt.Sprintf("> [%s] %s %s", reqID, r.Method, r.URL.String())) | ||
|
||
// Request Headers | ||
keys := make([]string, len(r.Header)) | ||
i := 0 | ||
for k := range r.Header { | ||
keys[i] = k | ||
i++ | ||
} | ||
sort.Strings(keys) | ||
for _, k := range keys { | ||
buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", reqID, k, strings.Join(r.Header[k], ", "))) | ||
} | ||
|
||
// Request parameters | ||
params := mux.Vars(r) | ||
keys = make([]string, len(params)) | ||
i = 0 | ||
for k := range params { | ||
keys[i] = k | ||
i++ | ||
} | ||
sort.Strings(keys) | ||
for _, k := range keys { | ||
buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", reqID, k, params[k])) | ||
} | ||
|
||
// Request body | ||
b, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
b = []byte("failed to read body: " + err.Error()) | ||
} | ||
if len(b) > 0 { | ||
buf.WriteByte('\n') | ||
lines := strings.Split(string(b), "\n") | ||
for _, line := range lines { | ||
buf.WriteString(fmt.Sprintf("[%s] %s\n", reqID, line)) | ||
} | ||
} | ||
r.Body = io.NopCloser(bytes.NewBuffer(b)) | ||
|
||
dupper := &responseDupper{ResponseWriter: rw, Buffer: &bytes.Buffer{}} | ||
h.ServeHTTP(dupper, r) | ||
|
||
buf.WriteString(fmt.Sprintf("\n< [%s] %s", reqID, http.StatusText(dupper.Status))) | ||
keys = make([]string, len(dupper.Header())) | ||
printResponseBody := true | ||
i = 0 | ||
for k, v := range dupper.Header() { | ||
if k == "Content-Type" && len(v) > 0 && v[0] == "application/x-7z-compressed" { | ||
printResponseBody = false | ||
} | ||
keys[i] = k | ||
i++ | ||
} | ||
sort.Strings(keys) | ||
for _, k := range keys { | ||
buf.WriteString(fmt.Sprintf("\n< [%s] %s: %s", reqID, k, strings.Join(dupper.Header()[k], ", "))) | ||
} | ||
if printResponseBody { | ||
buf.WriteByte('\n') | ||
lines := strings.Split(dupper.Buffer.String(), "\n") | ||
for _, line := range lines { | ||
buf.WriteString(fmt.Sprintf("[%s] %s\n", reqID, line)) | ||
} | ||
} | ||
buf.WriteByte('\n') | ||
_, err = w.Write(buf.Bytes()) // nolint: errcheck | ||
if err != nil { | ||
panic(err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// responseDupper tees the response to a buffer and a response writer. | ||
type responseDupper struct { | ||
http.ResponseWriter | ||
Buffer *bytes.Buffer | ||
Status int | ||
} | ||
|
||
// Write writes the data to the buffer and connection as part of an HTTP reply. | ||
func (r *responseDupper) Write(b []byte) (int, error) { | ||
return io.MultiWriter(r.ResponseWriter, r.Buffer).Write(b) | ||
} | ||
|
||
// WriteHeader records the status and sends an HTTP response header with status code. | ||
func (r *responseDupper) WriteHeader(s int) { | ||
r.Status = s | ||
r.ResponseWriter.WriteHeader(s) | ||
} | ||
|
||
// Hijack supports the http.Hijacker interface. | ||
func (r *responseDupper) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||
if hijacker, ok := r.ResponseWriter.(http.Hijacker); ok { | ||
return hijacker.Hijack() | ||
} | ||
return nil, nil, fmt.Errorf("debug middleware: inner ResponseWriter cannot be hijacked: %T", r.ResponseWriter) | ||
} | ||
|
||
// shortID produces a " unique" 6 bytes long string. | ||
// Do not use as a reliable way to get unique IDs, instead use for things like logging. | ||
func shortID() string { | ||
b := make([]byte, 6) | ||
io.ReadFull(rand.Reader, b) // nolint: errcheck | ||
return base64.RawURLEncoding.EncodeToString(b) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to wrap
goahttpmwr.Debug
with an additional layer that checks the content type and conditionally intercepts the request? If the request content type indicates a large body, you could temporarily replace the body with an empty reader, allowing it to pass through the middleware without printing the full body. After the middleware processes the request, then reassign the original body for further processing. Not sure if that's possible but it seems doable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's really not that simple because this middleware:
ServerHTTP
returns then it works on the response.And the
Content-Type
I check is the response one, not the request.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, sorry I missed that! Let me know if this other option works for you. This time, I used a different criteria: if the size of the debugging payload exceeds 16 KiB, then dump it into a file instead - but you could tweak the criterion as needed.
Tests: https://go.dev/play/p/N8QDLTkZbBA.
I'd prefer a solution that doesn't require us to vendor all that code if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This solution is better but it still does not fit the optimal solution for the client. Fundamentally the client wants be able to Debug the API when invoking the Download package endpoint, and by Debug we mean to be able to show request/response headers, params, status, etc.. without printing the response body (because it's a binary blob, we don't care about that).
Something like this would be the expected output in stdout:
Note that we print all information, skipping only the body being printed (but successfully downloaded).
Your current proposal will either print all the Debug information (including the body) or nothing at all. Basically the
io.Writer
that the middleware takes will write everything. That is why I decided to fork/vendor this specific middleware, I could not see how to achieve the objective without that approach.