Skip to content
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

Print the response's body on demand for debugging purposes #276

Merged
merged 12 commits into from
Jan 24, 2025
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ linters-settings:
arguments:
- fmt.Printf
- fmt.Println
- fmt.Fprint
shackra marked this conversation as resolved.
Show resolved Hide resolved
- fmt.Fprintf
- fmt.Fprintln
- os.Stderr.Sync
Expand Down
40 changes: 20 additions & 20 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions pkg/sync/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ const maxDepth = 8

var dontFixCycles, _ = strconv.ParseBool(os.Getenv("BATON_DONT_FIX_CYCLES"))

var (
ErrSyncNotComplete = fmt.Errorf("sync exited without finishing")
)
var ErrSyncNotComplete = fmt.Errorf("sync exited without finishing")

type Syncer interface {
Sync(context.Context) error
Expand Down Expand Up @@ -971,8 +969,7 @@ func (s *syncer) SyncAssets(ctx context.Context) error {
return nil
}

// SyncGrantExpansion
// TODO(morgabra) Docs.
// SyncGrantExpansion documentation pending.
func (s *syncer) SyncGrantExpansion(ctx context.Context) error {
l := ctxzap.Extract(ctx)
entitlementGraph := s.state.EntitlementGraph(ctx)
Expand Down
40 changes: 40 additions & 0 deletions pkg/uhttp/body_print.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package uhttp

// Implements a debugging facility for request responses. This changes
// the behavior of `BaseHttpClient` with an unexported flag.
//
// IMPORTANT: This feature is intended for development and debugging purposes only.
// Do not enable in production as it may expose sensitive information in logs.
//
// Usage:
// client := uhttp.NewBaseHttpClient(
// httpClient,
// uhttp.WithPrintBody(true), // Enable response body printing
// )

import (
"errors"
"fmt"
"io"
"os"
)

type printReader struct {
reader io.Reader
}

func (pr *printReader) Read(p []byte) (int, error) {
n, err := pr.reader.Read(p)
if n > 0 {
_, merr := fmt.Fprint(os.Stdout, string(p[:n]))
if merr != nil {
return -1, errors.Join(err, merr)
}
}

return n, err
}
shackra marked this conversation as resolved.
Show resolved Hide resolved

func wrapPrintBody(body io.Reader) io.Reader {
return &printReader{reader: body}
}
33 changes: 17 additions & 16 deletions pkg/uhttp/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"net/http"
"net/url"
"os"
"syscall"
"time"

Expand All @@ -33,8 +34,6 @@ const (
authorizationHeader = "Authorization"
)

const maxBodySize = 4096

type WrapperResponse struct {
Header http.Header
Body []byte
Expand Down Expand Up @@ -140,8 +139,8 @@ func WithJSONResponse(response interface{}) DoOption {

if !IsJSONContentType(contentHeader) {
if len(resp.Body) != 0 {
// we want to see the body regardless
return fmt.Errorf("unexpected content type for JSON response: %s. status code: %d. body: «%s»", contentHeader, resp.StatusCode, logBody(resp.Body, maxBodySize))
// to print the response, set the envvar BATON_DEBUG_PRINT_RESPONSE_BODY as non-empty, instead
return fmt.Errorf("unexpected content type for JSON response: %s. status code: %d", contentHeader, resp.StatusCode)
}
return fmt.Errorf("unexpected content type for JSON response: %s. status code: %d", contentHeader, resp.StatusCode)
}
Expand All @@ -150,7 +149,8 @@ func WithJSONResponse(response interface{}) DoOption {
}
err := json.Unmarshal(resp.Body, response)
if err != nil {
return fmt.Errorf("failed to unmarshal json response: %w. status code: %d. body %v", err, resp.StatusCode, logBody(resp.Body, maxBodySize))
// to print the response, set the envvar BATON_DEBUG_PRINT_RESPONSE_BODY as non-empty, instead
return fmt.Errorf("failed to unmarshal json response: %w. status code: %d", err, resp.StatusCode)
}
return nil
}
Expand All @@ -164,19 +164,13 @@ func WithAlwaysJSONResponse(response interface{}) DoOption {
}
err := json.Unmarshal(resp.Body, response)
if err != nil {
return fmt.Errorf("failed to unmarshal json response: %w. status code: %d. body %v", err, resp.StatusCode, logBody(resp.Body, maxBodySize))
// to print the response, set the envvar BATON_DEBUG_PRINT_RESPONSE_BODY as non-empty, instead
return fmt.Errorf("failed to unmarshal json response: %w. status code: %d", err, resp.StatusCode)
}
return nil
}
}

func logBody(body []byte, size int) string {
if len(body) > size {
return string(body[:size]) + " ..."
}
return string(body)
}

type ErrorResponse interface {
Message() string
}
Expand All @@ -190,12 +184,14 @@ func WithErrorResponse(resource ErrorResponse) DoOption {
contentHeader := resp.Header.Get(ContentType)

if !IsJSONContentType(contentHeader) {
return fmt.Errorf("unexpected content type for JSON error response: %s. status code: %d. body: «%s»", contentHeader, resp.StatusCode, logBody(resp.Body, maxBodySize))
// to print the response, set the envvar BATON_DEBUG_PRINT_RESPONSE_BODY as non-empty, instead
return fmt.Errorf("unexpected content type for JSON error response: %s. status code: %d", contentHeader, resp.StatusCode)
}

// Decode the JSON response body into the ErrorResponse
if err := json.Unmarshal(resp.Body, &resource); err != nil {
return fmt.Errorf("failed to unmarshal JSON error response: %w. status code: %d. body %v", err, resp.StatusCode, logBody(resp.Body, maxBodySize))
// to print the response, set the envvar BATON_DEBUG_PRINT_RESPONSE_BODY as non-empty, instead
return fmt.Errorf("failed to unmarshal JSON error response: %w. status code: %d", err, resp.StatusCode)
}

// Construct a more detailed error message
Expand Down Expand Up @@ -341,7 +337,12 @@ func (c *BaseHttpClient) Do(req *http.Request, options ...DoOption) (*http.Respo
}

// Replace resp.Body with a no-op closer so nobody has to worry about closing the reader.
resp.Body = io.NopCloser(bytes.NewBuffer(body))
shouldPrint := os.Getenv("BATON_DEBUG_PRINT_RESPONSE_BODY")
if shouldPrint != "" {
resp.Body = io.NopCloser(wrapPrintBody(bytes.NewBuffer(body)))
} else {
resp.Body = io.NopCloser(bytes.NewBuffer(body))
}

wresp := WrapperResponse{
Header: resp.Header,
Expand Down
Loading