feat: middleware reach, response writer interfaces, and CORS fixes

- Run global middleware for all requests, including OPTIONS preflights, NotFound, and MethodNotAllowed — previously bypassed by httprouter's internal handling
- Implement Hijacker, Flusher, and Pusher on the response writer for WebSocket, SSE, and HTTP/2 push support
- Fix CORS: echo a single matching origin, handle AllowCredentials with wildcard, append Vary: Origin
- Logger logs from a defer to capture correct status on panicked requests
- Static and StaticFS accept route middleware; add Context.AddHeader; warn on NotFound handler override
This commit is contained in:
2026-05-17 17:51:56 +02:00
parent 36c6d76e53
commit ae5d1f610a
7 changed files with 237 additions and 69 deletions

95
kite.go
View File

@@ -17,12 +17,16 @@ type Kite struct {
errorHandler ErrorHandler
mws []Middleware
customNotFound bool
serverReadTimeout time.Duration
serverWriteTimeout time.Duration
serverIdleTimeout time.Duration
serverShutdownTimeout time.Duration
}
type kiteContextKey struct{}
func New() *Kite {
k := &Kite{
r: httprouter.New(),
@@ -34,7 +38,7 @@ func New() *Kite {
}
k.SetErrorHandler(defaultErrorHandler)
k.SetNotFoundHandler(defaultNotFoundHandler)
k.setNotFoundHandler(defaultNotFoundHandler)
k.SetMethodNotAllowedHandler(defaultMethodNotAllowedHandler)
return k
@@ -44,17 +48,33 @@ func (k *Kite) SetErrorHandler(errorHandler ErrorHandler) {
k.errorHandler = errorHandler
}
func (k *Kite) SetNotFoundHandler(notFoundHandler NotFoundHandler) {
func (k *Kite) setNotFoundHandler(notFoundHandler NotFoundHandler) {
k.r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := &Context{ctx: r.Context(), w: newResponseWriter(w), r: r}
notFoundHandler(ctx)
ctx := r.Context().Value(kiteContextKey{}).(*Context)
if err := notFoundHandler(ctx); err != nil {
if err := k.errorHandler(ctx, err); err != nil {
log.Printf("[Kite] Error handler failed: %v\n", err)
}
}
})
}
func (k *Kite) SetNotFoundHandler(notFoundHandler NotFoundHandler) {
if k.customNotFound {
log.Println("[Kite] SetNotFoundHandler is overriding a previously configured NotFound handler")
}
k.setNotFoundHandler(notFoundHandler)
k.customNotFound = true
}
func (k *Kite) SetMethodNotAllowedHandler(methodNotAllowedHandler MethodNotAllowedHandler) {
k.r.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := &Context{ctx: r.Context(), w: newResponseWriter(w), r: r}
methodNotAllowedHandler(ctx)
ctx := r.Context().Value(kiteContextKey{}).(*Context)
if err := methodNotAllowedHandler(ctx); err != nil {
if err := k.errorHandler(ctx, err); err != nil {
log.Printf("[Kite] Error handler failed: %v\n", err)
}
}
})
}
@@ -77,7 +97,7 @@ func (k *Kite) SetServerShutdownTimeout(d time.Duration) {
func (k *Kite) Start(listenAddr string) error {
srv := &http.Server{
Addr: listenAddr,
Handler: k.r,
Handler: k.buildHandler(),
ReadTimeout: k.serverReadTimeout,
WriteTimeout: k.serverWriteTimeout,
@@ -105,7 +125,7 @@ func (k *Kite) Start(listenAddr string) error {
func (k *Kite) StartTLS(listenAddr, certFile, keyFile string) error {
srv := &http.Server{
Addr: listenAddr,
Handler: k.r,
Handler: k.buildHandler(),
ReadTimeout: k.serverReadTimeout,
WriteTimeout: k.serverWriteTimeout,
@@ -135,6 +155,11 @@ func (k *Kite) SPA(root string) {
}
func (k *Kite) SPAFS(fs http.FileSystem) {
if k.customNotFound {
log.Println("[Kite] SPAFS is overriding a previously configured NotFound handler")
}
k.customNotFound = true
fileServer := http.FileServer(fs)
k.r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -150,18 +175,24 @@ func (k *Kite) SPAFS(fs http.FileSystem) {
})
}
func (k *Kite) Static(prefix, root string) {
k.StaticFS(prefix, http.Dir(root))
func (k *Kite) Static(prefix, root string, mws ...Middleware) {
k.StaticFS(prefix, http.Dir(root), mws...)
}
func (k *Kite) StaticFS(prefix string, fs http.FileSystem) {
func (k *Kite) StaticFS(prefix string, fs http.FileSystem, mws ...Middleware) {
if prefix == "" || prefix[len(prefix)-1] != '/' {
prefix += "/"
}
handler := http.StripPrefix(prefix, http.FileServer(fs))
k.r.Handler(http.MethodGet, prefix+"*filepath", handler)
k.r.Handler(http.MethodHead, prefix+"*filepath", handler)
h := func(ctx *Context) error {
handler.ServeHTTP(ctx.w, ctx.r)
return nil
}
k.handle(http.MethodGet, prefix+"*filepath", h, mws...)
k.handle(http.MethodHead, prefix+"*filepath", h, mws...)
}
func (k *Kite) Group(prefix string, mws ...Middleware) *Group {
@@ -208,25 +239,41 @@ func (k *Kite) TRACE(path string, h Handler, mws ...Middleware) {
k.handle(http.MethodTrace, path, h, mws...)
}
func (k *Kite) buildHandler() http.Handler {
inner := func(ctx *Context) error {
ctx.r = ctx.r.WithContext(context.WithValue(ctx.r.Context(), kiteContextKey{}, ctx))
k.r.ServeHTTP(ctx.w, ctx.r)
return nil
}
wrapped := inner
for i := len(k.mws) - 1; i >= 0; i-- {
wrapped = k.mws[i](wrapped)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := &Context{
w: newResponseWriter(w),
r: r,
}
if err := wrapped(ctx); err != nil {
if err := k.errorHandler(ctx, err); err != nil {
log.Printf("[Kite] Error handler failed: %v\n", err)
}
}
})
}
func (k *Kite) handle(method, path string, h Handler, mws ...Middleware) {
k.r.Handle(method, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := &Context{
ctx: r.Context(),
w: newResponseWriter(w),
r: r,
p: p,
}
ctx := r.Context().Value(kiteContextKey{}).(*Context)
ctx.p = p
wrappedHandler := h
for i := len(mws) - 1; i >= 0; i-- {
wrappedHandler = mws[i](wrappedHandler)
}
for i := len(k.mws) - 1; i >= 0; i-- {
wrappedHandler = k.mws[i](wrappedHandler)
}
if err := wrappedHandler(ctx); err != nil {
if err := k.errorHandler(ctx, err); err != nil {
log.Printf("[Kite] Error handler failed: %v\n", err)