-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
176 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package httputil | ||
|
||
import "net/http" | ||
|
||
// Chain applies middlewares to a http.Handler | ||
func Chain(h http.Handler, ff ...func(http.Handler) http.Handler) http.Handler { | ||
for _, f := range ff { | ||
h = f(h) | ||
} | ||
return h | ||
} | ||
|
||
// ChainFunc applies middlewares to a http.HandlerFunc | ||
func ChainFunc(hf http.HandlerFunc, ff ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc { | ||
for _, f := range ff { | ||
hf = f(hf) | ||
} | ||
return hf | ||
} |
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,50 @@ | ||
package hlog | ||
|
||
import ( | ||
"log/slog" | ||
"net/http" | ||
"time" | ||
|
||
"go.adoublef.dev/sdk/net/http/httputil" | ||
) | ||
|
||
// Log wraps a http.Handler with a request logger. | ||
func Log(h http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
rw := httputil.Wrap(w, r) | ||
|
||
start := time.Now() | ||
defer func() { | ||
duration := time.Since(start) | ||
|
||
slog.LogAttrs( | ||
r.Context(), | ||
statusLevel(rw.Status()), | ||
"http request", | ||
slog.String("method", r.Method), | ||
slog.Int64("time_ms", int64(duration/time.Millisecond)), | ||
slog.String("path", r.URL.Path), | ||
slog.Int("status", rw.Status()), | ||
slog.String("duration", duration.String()), | ||
) | ||
}() | ||
|
||
h.ServeHTTP(rw, r) | ||
}) | ||
} | ||
|
||
func statusLevel(status int) slog.Level { | ||
switch { | ||
case status <= 0: | ||
return slog.LevelWarn | ||
case status < 400: // for codes in 100s, 200s, 300s | ||
return slog.LevelInfo | ||
case status >= 400 && status < 500: | ||
// switching to info level to be less noisy | ||
return slog.LevelInfo | ||
case status >= 500: | ||
return slog.LevelError | ||
default: | ||
return slog.LevelInfo | ||
} | ||
} |
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,107 @@ | ||
package httputil | ||
|
||
import ( | ||
"bufio" | ||
"errors" | ||
"net" | ||
"net/http" | ||
) | ||
|
||
type ResponseWriter interface { | ||
http.ResponseWriter | ||
http.Flusher | ||
// Status returns the status code of the response or 0 if the response has not been written. | ||
Status() int | ||
// Written returns whether or not the ResponseWriter has been written. | ||
Written() bool | ||
// Size returns the size of the response body. | ||
Size() int | ||
Unwrap() http.ResponseWriter | ||
} | ||
|
||
type response struct { | ||
http.ResponseWriter | ||
method string | ||
status int | ||
size int | ||
} | ||
|
||
// Size implements ResponseWriter. | ||
func (rw *response) Size() int { | ||
return rw.size | ||
} | ||
|
||
// Unwrap implements ResponseWriter. | ||
func (rw *response) Unwrap() http.ResponseWriter { | ||
return rw.ResponseWriter | ||
} | ||
|
||
// Written implements ResponseWriter. | ||
func (rw *response) Written() bool { | ||
return rw.status != 0 | ||
} | ||
|
||
// Status implements ResponseWriter. | ||
func (rw *response) Status() int { | ||
return rw.status | ||
} | ||
|
||
// Write implements ResponseWriter. | ||
// Subtle: this method shadows the method (ResponseWriter).Write of response.ResponseWriter. | ||
func (rw *response) Write(b []byte) (size int, err error) { | ||
if !rw.Written() { | ||
// The status will be StatusOK if WriteHeader has not been called yet | ||
rw.WriteHeader(http.StatusOK) | ||
} | ||
if rw.method != http.MethodHead { | ||
size, err = rw.ResponseWriter.Write(b) | ||
rw.size += size | ||
} | ||
return size, err | ||
} | ||
|
||
// WriteHeader implements ResponseWriter. | ||
// Subtle: this method shadows the method (ResponseWriter).WriteHeader of response.ResponseWriter. | ||
func (rw *response) WriteHeader(s int) { | ||
// Avoid panic if status code is not a valid HTTP status code | ||
if s < 100 || s > 999 { | ||
rw.ResponseWriter.WriteHeader(500) | ||
rw.status = 500 | ||
return | ||
} | ||
|
||
rw.ResponseWriter.WriteHeader(s) | ||
rw.status = s | ||
} | ||
|
||
func (rw *response) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||
hijacker, ok := rw.ResponseWriter.(http.Hijacker) | ||
if !ok { | ||
return nil, nil, ErrHijackUnsupported | ||
} | ||
|
||
conn, brw, err := hijacker.Hijack() | ||
if err == nil { | ||
rw.status = -1 | ||
} | ||
|
||
return conn, brw, err | ||
} | ||
|
||
func (rw *response) Flush() { | ||
if flusher, ok := rw.ResponseWriter.(http.Flusher); ok { | ||
flusher.Flush() | ||
} | ||
} | ||
|
||
// Wrap | ||
func Wrap(w http.ResponseWriter, r *http.Request) ResponseWriter { | ||
if rw, ok := w.(ResponseWriter); ok { | ||
return rw | ||
} | ||
return &response{w, r.Method, 0, 0} | ||
} | ||
|
||
var ( | ||
ErrHijackUnsupported = errors.New("the ResponseWriter doesn't support the Hijacker interface") | ||
) |