-
Notifications
You must be signed in to change notification settings - Fork 22
/
mux.go
122 lines (106 loc) · 3.26 KB
/
mux.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Package triemux implements an HTTP multiplexer, or URL router, which can be
// used to serve responses from multiple distinct handlers within a single URL
// hierarchy.
package triemux
import (
"net/http"
"regexp"
"strings"
"sync"
"github.com/alphagov/router/handlers"
"github.com/alphagov/router/logger"
"github.com/alphagov/router/trie"
)
type Mux struct {
mu sync.RWMutex
exactTrie *trie.Trie[http.Handler]
prefixTrie *trie.Trie[http.Handler]
count int
downcaser http.Handler
}
// NewMux makes a new empty Mux.
func NewMux() *Mux {
return &Mux{
exactTrie: trie.NewTrie[http.Handler](),
prefixTrie: trie.NewTrie[http.Handler](),
downcaser: handlers.NewDowncaseRedirectHandler(),
}
}
// ServeHTTP forwards the request to a backend with a registered route matching
// the request path. Serves 404 when there is no backend. Serves 301 redirect
// to lowercase path when the URL path is entirely uppercase. Serves 503 when
// no routes are loaded.
func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if mux.count == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
logger.NotifySentry(logger.ReportableError{
Error: logger.RecoveredError{ErrorMessage: "route table is empty"},
Request: r,
})
internalServiceUnavailableCountMetric.Inc()
return
}
if shouldRedirToLowercasePath(r.URL.Path) {
mux.downcaser.ServeHTTP(w, r)
return
}
handler, ok := mux.lookup(r.URL.Path)
if !ok {
http.NotFound(w, r)
return
}
handler.ServeHTTP(w, r)
}
// shouldRedirToLowercasePath takes a URL path string (such as "/government/guidance")
// and returns:
// - true, if path is in all caps; for example:
// "/GOVERNMENT/GUIDANCE" -> true (should redirect to "/government/guidance")
// - false, otherwise; for example:
// "/GoVeRnMeNt/gUiDaNcE" -> false (should forward "/GoVeRnMeNt/gUiDaNcE" as-is)
func shouldRedirToLowercasePath(path string) (match bool) {
match, _ = regexp.MatchString(`^\/[A-Z]+[A-Z\W\d]+$`, path)
return
}
// lookup finds a URL path in the Mux and returns the corresponding handler.
func (mux *Mux) lookup(path string) (handler http.Handler, ok bool) {
mux.mu.RLock()
defer mux.mu.RUnlock()
pathSegments := splitPath(path)
if handler, ok = mux.exactTrie.Get(pathSegments); !ok {
handler, ok = mux.prefixTrie.GetLongestPrefix(pathSegments)
}
if !ok {
entryNotFoundCountMetric.Inc()
return nil, false
}
return
}
// Handle adds a route (either an exact path or a path prefix) to the Mux and
// and associates it with a handler, so that the Mux will pass matching
// requests to that handler.
func (mux *Mux) Handle(path string, prefix bool, handler http.Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
t := mux.exactTrie
if prefix {
t = mux.prefixTrie
}
t.Set(splitPath(path), handler)
mux.count++
}
func (mux *Mux) RouteCount() int {
return mux.count
}
// splitPath turns a slash-delimited string into a lookup path (a slice
// containing the strings between slashes). splitPath omits empty items
// produced by leading, trailing, or adjacent slashes.
func splitPath(path string) []string {
partsWithBlanks := strings.Split(path, "/")
parts := make([]string, 0, len(partsWithBlanks))
for _, part := range partsWithBlanks {
if part != "" {
parts = append(parts, part)
}
}
return parts
}