105 lines
2.9 KiB
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
|
|
}
|