Files
NianAIGC/backend/internal/httpapi/method_compat.go

105 lines
2.9 KiB
Go

package httpapi
import (
"net/http"
"sort"
"strings"
)
type routeMethodPattern struct {
path string
methods map[string]struct{}
allow []string
}
// WithRouteMethodCompatibility applies the legacy-compatible method matrix
// for the checked-in public HTTP contract. It sits outside authentication so
// OPTIONS never depends on runtime configuration and HEAD executes the
// corresponding GET semantics while suppressing the response body.
func WithRouteMethodCompatibility(next http.Handler) http.Handler {
patterns := routeMethodPatterns(GoRouteSurface())
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pattern := matchingRouteMethodPattern(patterns, r.URL.Path)
if pattern == nil {
next.ServeHTTP(w, r)
return
}
if r.Method == http.MethodOptions {
w.Header().Set("Allow", strings.Join(pattern.allow, ", "))
w.WriteHeader(http.StatusNoContent)
return
}
if r.Method == http.MethodHead {
if _, ok := pattern.methods[http.MethodHead]; !ok {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
clone := r.Clone(r.Context())
clone.Method = http.MethodGet
next.ServeHTTP(&methodHeadResponseWriter{ResponseWriter: w}, clone)
return
}
if _, ok := pattern.methods[r.Method]; !ok {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
next.ServeHTTP(w, r)
})
}
func routeMethodPatterns(surface []RouteSurface) []routeMethodPattern {
byPath := make(map[string]int)
patterns := make([]routeMethodPattern, 0)
for _, route := range surface {
index, exists := byPath[route.Path]
if !exists {
index = len(patterns)
byPath[route.Path] = index
patterns = append(patterns, routeMethodPattern{path: route.Path, methods: make(map[string]struct{})})
}
patterns[index].methods[route.Method] = struct{}{}
}
for index := range patterns {
if _, hasGet := patterns[index].methods[http.MethodGet]; hasGet {
patterns[index].methods[http.MethodHead] = struct{}{}
}
patterns[index].methods[http.MethodOptions] = struct{}{}
patterns[index].allow = make([]string, 0, len(patterns[index].methods))
for method := range patterns[index].methods {
patterns[index].allow = append(patterns[index].allow, method)
}
sort.Strings(patterns[index].allow)
}
return patterns
}
func matchingRouteMethodPattern(patterns []routeMethodPattern, path string) *routeMethodPattern {
for index := range patterns {
if MatchSurfacePath(patterns[index].path, path) {
return &patterns[index]
}
}
return nil
}
type methodHeadResponseWriter struct {
http.ResponseWriter
wroteHeader bool
}
func (writer *methodHeadResponseWriter) WriteHeader(status int) {
if writer.wroteHeader {
return
}
writer.wroteHeader = true
writer.ResponseWriter.WriteHeader(status)
}
func (writer *methodHeadResponseWriter) Write(body []byte) (int, error) {
if !writer.wroteHeader {
writer.WriteHeader(http.StatusOK)
}
return len(body), nil
}