13// NewHTTPClient creates an HTTP client with debug logging enabled when debug mode is on.
14func NewHTTPClient() *http.Client {
16 Transport: &HTTPRoundTripLogger{
17 Transport: http.DefaultTransport,
22// HTTPRoundTripLogger is an http.RoundTripper that logs requests and responses.
23type HTTPRoundTripLogger struct {
24 Transport http.RoundTripper
27// RoundTrip implements http.RoundTripper interface with logging.
28func (h *HTTPRoundTripLogger) RoundTrip(req *http.Request) (*http.Response, error) {
30 var save io.ReadCloser
31 save, req.Body, err = drainBody(req.Body)
34 "HTTP request failed",
42 if slog.Default().Enabled(req.Context(), slog.LevelDebug) {
47 "body", bodyToString(save),
52 resp, err := h.Transport.RoundTrip(req)
53 duration := time.Since(start)
56 "HTTP request failed",
59 "duration_ms", duration.Milliseconds(),
65 save, resp.Body, err = drainBody(resp.Body)
66 if slog.Default().Enabled(req.Context(), slog.LevelDebug) {
69 "status_code", resp.StatusCode,
70 "status", resp.Status,
71 "headers", formatHeaders(resp.Header),
72 "body", bodyToString(save),
73 "content_length", resp.ContentLength,
74 "duration_ms", duration.Milliseconds(),
81func bodyToString(body io.ReadCloser) string {
85 src, err := io.ReadAll(body)
87 slog.Error("Failed to read body", "error", err)
91 if json.Indent(&b, bytes.TrimSpace(src), "", " ") != nil {
98// formatHeaders formats HTTP headers for logging, filtering out sensitive information.
99func formatHeaders(headers http.Header) map[string][]string {
100 filtered := make(map[string][]string)
101 for key, values := range headers {
102 lowerKey := strings.ToLower(key)
103 // Filter out sensitive headers
104 if strings.Contains(lowerKey, "authorization") ||
105 strings.Contains(lowerKey, "api-key") ||
106 strings.Contains(lowerKey, "token") ||
107 strings.Contains(lowerKey, "secret") {
108 filtered[key] = []string{"[REDACTED]"}
110 filtered[key] = values
116func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
117 if b == nil || b == http.NoBody {
118 return http.NoBody, http.NoBody, nil
121 if _, err = buf.ReadFrom(b); err != nil {
124 if err = b.Close(); err != nil {
127 return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list