Initial commit
This commit is contained in:
commit
89053153cc
8152 files changed
+2086461
No files matched your search
@@ -0,0 +1,107 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
// middleware wraps an http.Handler. Keep middleware transport-level;
|
||||
// business concerns belong in services.
|
||||
type middleware func(http.Handler) http.Handler
|
||||
|
||||
// chain composes middlewares so chain(h, a, b) == a(b(h)): the first
|
||||
// argument runs outermost.
|
||||
func chain(h http.Handler, mws ...middleware) http.Handler {
|
||||
for i := len(mws) - 1; i >= 0; i-- {
|
||||
h = mws[i](h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// logging records one line per request after it completes.
|
||||
func logging(logger *slog.Logger) middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||
next.ServeHTTP(rec, r)
|
||||
logger.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", rec.status,
|
||||
"duration_ms", time.Since(start).Milliseconds(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// recoverPanics turns panics into 500s instead of dropped connections.
|
||||
func recoverPanics(logger *slog.Logger) middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
logger.Error("panic recovered", "panic", v, "stack", string(debug.Stack()))
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cors lets the configured origins call the API from a browser (e.g. the
|
||||
// Vite dev server on :5173). A "*" entry allows every origin.
|
||||
func cors(allowed []string) middleware {
|
||||
set := make(map[string]struct{}, len(allowed))
|
||||
allowAll := false
|
||||
for _, o := range allowed {
|
||||
if o == "*" {
|
||||
allowAll = true
|
||||
}
|
||||
set[o] = struct{}{}
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if _, ok := set[origin]; origin != "" && (allowAll || ok) {
|
||||
h := w.Header()
|
||||
h.Set("Access-Control-Allow-Origin", origin)
|
||||
h.Add("Vary", "Origin")
|
||||
h.Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
|
||||
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
h.Set("Access-Control-Max-Age", "600")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// timeout bounds each request's handler time. The body is the raw timeout
|
||||
// response — http.TimeoutHandler does not set a content type for it.
|
||||
func timeout(d time.Duration) middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.TimeoutHandler(next, d,
|
||||
`{"error":{"code":"timeout","message":"request timed out"}}`)
|
||||
}
|
||||
}
|
||||
|
||||
// statusRecorder captures the response status for logging.
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (r *statusRecorder) WriteHeader(code int) {
|
||||
r.status = code
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Unwrap lets http.ResponseController reach the underlying writer.
|
||||
func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
|
||||
Reference in new issue
Block a user