feat: complete remaining Go backend modules
This commit is contained in:
104
backend/internal/httpapi/method_compat.go
Normal file
104
backend/internal/httpapi/method_compat.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type routeMethodPattern struct {
|
||||
path string
|
||||
methods map[string]struct{}
|
||||
allow []string
|
||||
}
|
||||
|
||||
// WithRouteMethodCompatibility applies the method behavior that Next derives
|
||||
// from app route exports. 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
|
||||
}
|
||||
Reference in New Issue
Block a user