Skip to content

Commit eab5bda

Browse files
authored
Replace API Debug Middleware (#631)
1 parent b18c703 commit eab5bda

File tree

2 files changed

+154
-1
lines changed

2 files changed

+154
-1
lines changed

internal/api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func HTTPServer(
9090
handler = versionHeaderMiddleware(config.AppVersion)(handler)
9191
if config.Debug {
9292
handler = goahttpmwr.Log(loggerAdapter(logger))(handler)
93-
handler = goahttpmwr.Debug(mux, os.Stdout)(handler)
93+
handler = debug(mux, os.Stdout)(handler)
9494
}
9595

9696
return &http.Server{

internal/api/debug_middleware.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// This file is based on `debug.go` from the Goa project:
2+
// https://github.com/goadesign/goa/blob/v3/http/middleware/debug.go
3+
//
4+
// Copyright (c) 2015 Raphaël Simon
5+
// Licensed under the MIT License:
6+
// https://github.com/goadesign/goa/blob/v3/LICENSE
7+
//
8+
// Modifications have been made from the original version. Namely, to solve an
9+
// issue where response bodies of type `application/x-7z-compressed` should not
10+
// be printed, this copy of the middleware was created and modified accordingly,
11+
// as the original debug middleware in the Goa library could not be directly
12+
// altered.
13+
14+
package api
15+
16+
import (
17+
"bufio"
18+
"bytes"
19+
"crypto/rand"
20+
"encoding/base64"
21+
"fmt"
22+
"io"
23+
"net"
24+
"net/http"
25+
"sort"
26+
"strings"
27+
28+
goahttp "goa.design/goa/v3/http"
29+
"goa.design/goa/v3/middleware"
30+
)
31+
32+
// debug returns a debug middleware which prints detailed information about
33+
// incoming requests and outgoing responses including all headers, parameters
34+
// and bodies.
35+
func debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler {
36+
return func(h http.Handler) http.Handler {
37+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
38+
buf := &bytes.Buffer{}
39+
// Request ID
40+
reqID := r.Context().Value(middleware.RequestIDKey)
41+
if reqID == nil {
42+
reqID = shortID()
43+
}
44+
45+
// Request URL
46+
buf.WriteString(fmt.Sprintf("> [%s] %s %s", reqID, r.Method, r.URL.String()))
47+
48+
// Request Headers
49+
keys := make([]string, len(r.Header))
50+
i := 0
51+
for k := range r.Header {
52+
keys[i] = k
53+
i++
54+
}
55+
sort.Strings(keys)
56+
for _, k := range keys {
57+
buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", reqID, k, strings.Join(r.Header[k], ", ")))
58+
}
59+
60+
// Request parameters
61+
params := mux.Vars(r)
62+
keys = make([]string, len(params))
63+
i = 0
64+
for k := range params {
65+
keys[i] = k
66+
i++
67+
}
68+
sort.Strings(keys)
69+
for _, k := range keys {
70+
buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", reqID, k, params[k]))
71+
}
72+
73+
// Request body
74+
b, err := io.ReadAll(r.Body)
75+
if err != nil {
76+
b = []byte("failed to read body: " + err.Error())
77+
}
78+
if len(b) > 0 {
79+
buf.WriteByte('\n')
80+
lines := strings.Split(string(b), "\n")
81+
for _, line := range lines {
82+
buf.WriteString(fmt.Sprintf("[%s] %s\n", reqID, line))
83+
}
84+
}
85+
r.Body = io.NopCloser(bytes.NewBuffer(b))
86+
87+
dupper := &responseDupper{ResponseWriter: rw, Buffer: &bytes.Buffer{}}
88+
h.ServeHTTP(dupper, r)
89+
90+
buf.WriteString(fmt.Sprintf("\n< [%s] %s", reqID, http.StatusText(dupper.Status)))
91+
keys = make([]string, len(dupper.Header()))
92+
printResponseBody := true
93+
i = 0
94+
for k, v := range dupper.Header() {
95+
if k == "Content-Type" && len(v) > 0 && v[0] == "application/x-7z-compressed" {
96+
printResponseBody = false
97+
}
98+
keys[i] = k
99+
i++
100+
}
101+
sort.Strings(keys)
102+
for _, k := range keys {
103+
buf.WriteString(fmt.Sprintf("\n< [%s] %s: %s", reqID, k, strings.Join(dupper.Header()[k], ", ")))
104+
}
105+
if printResponseBody {
106+
buf.WriteByte('\n')
107+
lines := strings.Split(dupper.Buffer.String(), "\n")
108+
for _, line := range lines {
109+
buf.WriteString(fmt.Sprintf("[%s] %s\n", reqID, line))
110+
}
111+
}
112+
buf.WriteByte('\n')
113+
_, err = w.Write(buf.Bytes()) // nolint: errcheck
114+
if err != nil {
115+
panic(err)
116+
}
117+
})
118+
}
119+
}
120+
121+
// responseDupper tees the response to a buffer and a response writer.
122+
type responseDupper struct {
123+
http.ResponseWriter
124+
Buffer *bytes.Buffer
125+
Status int
126+
}
127+
128+
// Write writes the data to the buffer and connection as part of an HTTP reply.
129+
func (r *responseDupper) Write(b []byte) (int, error) {
130+
return io.MultiWriter(r.ResponseWriter, r.Buffer).Write(b)
131+
}
132+
133+
// WriteHeader records the status and sends an HTTP response header with status code.
134+
func (r *responseDupper) WriteHeader(s int) {
135+
r.Status = s
136+
r.ResponseWriter.WriteHeader(s)
137+
}
138+
139+
// Hijack supports the http.Hijacker interface.
140+
func (r *responseDupper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
141+
if hijacker, ok := r.ResponseWriter.(http.Hijacker); ok {
142+
return hijacker.Hijack()
143+
}
144+
return nil, nil, fmt.Errorf("debug middleware: inner ResponseWriter cannot be hijacked: %T", r.ResponseWriter)
145+
}
146+
147+
// shortID produces a " unique" 6 bytes long string.
148+
// Do not use as a reliable way to get unique IDs, instead use for things like logging.
149+
func shortID() string {
150+
b := make([]byte, 6)
151+
io.ReadFull(rand.Reader, b) // nolint: errcheck
152+
return base64.RawURLEncoding.EncodeToString(b)
153+
}

0 commit comments

Comments
 (0)