Skip to content

Commit 4dd3ade

Browse files
authored
Merge pull request #84 from netlify/add-router
Add a generic router interface
2 parents eb9153e + ac0aaab commit 4dd3ade

File tree

8 files changed

+648
-105
lines changed

8 files changed

+648
-105
lines changed

go.mod

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,41 @@
11
module github.com/netlify/netlify-commons
22

33
require (
4-
github.com/BurntSushi/toml v0.3.0
5-
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
4+
github.com/BurntSushi/toml v0.3.1
65
github.com/bitly/go-simplejson v0.5.0 // indirect
76
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
8-
github.com/bugsnag/bugsnag-go v1.3.0
9-
github.com/bugsnag/panicwrap v0.0.0-20180510051541-1d162ee1264c // indirect
7+
github.com/bugsnag/bugsnag-go v1.5.1
8+
github.com/bugsnag/panicwrap v1.2.0 // indirect
109
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
11-
github.com/go-sql-driver/mysql v1.4.1 // indirect
12-
github.com/gogo/protobuf v1.2.1 // indirect
13-
github.com/golang/protobuf v1.3.1 // indirect
14-
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
15-
github.com/hashicorp/go-msgpack v0.5.3 // indirect
16-
github.com/hashicorp/hcl v0.0.0-20180906183839-65a6292f0157 // indirect
17-
github.com/hashicorp/raft v1.0.0 // indirect
18-
github.com/inconshreveable/mousetrap v1.0.0 // indirect
19-
github.com/joho/godotenv v1.2.0
20-
github.com/juju/loggo v0.0.0-20190212223446-d976af380377 // indirect
10+
github.com/go-chi/chi v4.0.2+incompatible
11+
github.com/gofrs/uuid v3.2.0+incompatible // indirect
12+
github.com/joho/godotenv v1.3.0
2113
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
2214
github.com/kelseyhightower/envconfig v1.3.0
15+
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
2316
github.com/kr/pretty v0.1.0 // indirect
24-
github.com/lib/pq v1.0.0 // indirect
25-
github.com/magiconair/properties v0.0.0-20190110142458-7757cc9fdb85 // indirect
26-
github.com/mitchellh/mapstructure v1.1.2 // indirect
2717
github.com/nats-io/gnatsd v1.4.1 // indirect
2818
github.com/nats-io/go-nats v1.3.0
2919
github.com/nats-io/go-nats-streaming v0.3.4
30-
github.com/nats-io/nats-streaming-server v0.12.2 // indirect
31-
github.com/nats-io/nuid v0.0.0-20180712044959-3024a71c3cbe // indirect
32-
github.com/onsi/ginkgo v1.8.0 // indirect
33-
github.com/onsi/gomega v1.5.0 // indirect
20+
github.com/nats-io/nats-server v1.4.1 // indirect
21+
github.com/nats-io/nats-streaming-server v0.15.1 // indirect
3422
github.com/opentracing/opentracing-go v1.1.0
35-
github.com/pascaldekloe/goe v0.1.0 // indirect
36-
github.com/pelletier/go-toml v0.0.0-20181124002727-27c6b39a135b // indirect
23+
github.com/pelletier/go-toml v1.3.0 // indirect
3724
github.com/philhofer/fwd v1.0.0 // indirect
38-
github.com/pkg/errors v0.8.0
39-
github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872 // indirect
25+
github.com/pkg/errors v0.8.1
26+
github.com/rs/cors v1.6.0
4027
github.com/satori/go.uuid v1.2.0
28+
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
4129
github.com/shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d
42-
github.com/sirupsen/logrus v1.0.5
43-
github.com/spf13/afero v1.2.1 // indirect
44-
github.com/spf13/cast v1.3.0 // indirect
45-
github.com/spf13/cobra v0.0.1
30+
github.com/sirupsen/logrus v1.4.1
31+
github.com/spf13/afero v1.2.2 // indirect
32+
github.com/spf13/cobra v0.0.4-0.20190321000552-67fc4837d267
4633
github.com/spf13/jwalterweatherman v1.1.0 // indirect
47-
github.com/spf13/pflag v1.0.0 // indirect
48-
github.com/spf13/viper v1.0.2
49-
github.com/stretchr/testify v1.2.2
34+
github.com/spf13/viper v1.3.2
35+
github.com/stretchr/testify v1.3.0
5036
github.com/tinylib/msgp v1.1.0 // indirect
51-
go.etcd.io/bbolt v1.3.2 // indirect
52-
golang.org/x/crypto v0.0.0-20161031180806-9477e0b78b9a // indirect
53-
google.golang.org/appengine v1.5.0 // indirect
37+
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f // indirect
38+
golang.org/x/text v0.3.2 // indirect
5439
gopkg.in/DataDog/dd-trace-go.v1 v1.12.1
55-
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
56-
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
57-
gopkg.in/yaml.v2 v2.2.2 // indirect
40+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
5841
)

go.sum

Lines changed: 114 additions & 67 deletions
Large diffs are not rendered by default.

router/errors.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package router
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/netlify/netlify-commons/tracing"
8+
)
9+
10+
// HTTPError is an error with a message and an HTTP status code.
11+
type HTTPError struct {
12+
Code int `json:"code"`
13+
Message string `json:"msg"`
14+
JSON interface{} `json:"json"`
15+
InternalError error `json:"-"`
16+
InternalMessage string `json:"-"`
17+
ErrorID string `json:"error_id,omitempty"`
18+
}
19+
20+
// BadRequestError creates a 400 HTTP error
21+
func BadRequestError(fmtString string, args ...interface{}) *HTTPError {
22+
return httpError(http.StatusBadRequest, fmtString, args...)
23+
}
24+
25+
// InternalServerError creates a 500 HTTP error
26+
func InternalServerError(fmtString string, args ...interface{}) *HTTPError {
27+
return httpError(http.StatusInternalServerError, fmtString, args...)
28+
}
29+
30+
// NotFoundError creates a 404 HTTP error
31+
func NotFoundError(fmtString string, args ...interface{}) *HTTPError {
32+
return httpError(http.StatusNotFound, fmtString, args...)
33+
}
34+
35+
// UnauthorizedError creates a 401 HTTP error
36+
func UnauthorizedError(fmtString string, args ...interface{}) *HTTPError {
37+
return httpError(http.StatusUnauthorized, fmtString, args...)
38+
}
39+
40+
// UnavailableServiceError creates a 503 HTTP error
41+
func UnavailableServiceError(fmtString string, args ...interface{}) *HTTPError {
42+
return httpError(http.StatusServiceUnavailable, fmtString, args...)
43+
}
44+
45+
// Error will describe the HTTP error in text
46+
func (e *HTTPError) Error() string {
47+
if e.InternalMessage != "" {
48+
return e.InternalMessage
49+
}
50+
return fmt.Sprintf("%d: %s", e.Code, e.Message)
51+
}
52+
53+
// Cause will return the root cause error
54+
func (e *HTTPError) Cause() error {
55+
if e.InternalError != nil {
56+
return e.InternalError
57+
}
58+
return e
59+
}
60+
61+
// WithJSONError will add json details to the error
62+
func (e *HTTPError) WithJSONError(json interface{}) *HTTPError {
63+
e.JSON = json
64+
return e
65+
}
66+
67+
// WithInternalError will add internal error information to an error
68+
func (e *HTTPError) WithInternalError(err error) *HTTPError {
69+
e.InternalError = err
70+
return e
71+
}
72+
73+
// WithInternalMessage will add and internal message to an error
74+
func (e *HTTPError) WithInternalMessage(fmtString string, args ...interface{}) *HTTPError {
75+
e.InternalMessage = fmt.Sprintf(fmtString, args...)
76+
return e
77+
}
78+
79+
func httpError(code int, fmtString string, args ...interface{}) *HTTPError {
80+
return &HTTPError{
81+
Code: code,
82+
Message: fmt.Sprintf(fmtString, args...),
83+
}
84+
}
85+
86+
// HandleError will handle an error
87+
func HandleError(err error, w http.ResponseWriter, r *http.Request) {
88+
log := tracing.GetLogger(r)
89+
errorID := tracing.RequestID(r)
90+
switch e := err.(type) {
91+
case *HTTPError:
92+
if e.Code >= http.StatusInternalServerError {
93+
e.ErrorID = errorID
94+
// this will get us the stack trace too
95+
log.WithError(e.Cause()).Error(e.Error())
96+
} else {
97+
log.WithError(e.Cause()).Info(e.Error())
98+
}
99+
100+
if jsonErr := SendJSON(w, e.Code, e); jsonErr != nil {
101+
HandleError(jsonErr, w, r)
102+
}
103+
default:
104+
log.WithError(e).Errorf("Unhandled server error: %s", e.Error())
105+
// hide real error details from response to prevent info leaks
106+
w.WriteHeader(http.StatusInternalServerError)
107+
if _, writeErr := w.Write([]byte(`{"code":500,"msg":"Internal server error","error_id":"` + errorID + `"}`)); writeErr != nil {
108+
log.WithError(writeErr).Error("Error writing generic error message")
109+
}
110+
}
111+
}

router/helpers.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package router
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/pkg/errors"
9+
)
10+
11+
// SendJSON will write the response object as JSON
12+
func SendJSON(w http.ResponseWriter, status int, obj interface{}) error {
13+
w.Header().Set("Content-Type", "application/json")
14+
b, err := json.Marshal(obj)
15+
if err != nil {
16+
return errors.Wrap(err, fmt.Sprintf("Error encoding json response: %v", obj))
17+
}
18+
w.WriteHeader(status)
19+
_, err = w.Write(b)
20+
return err
21+
}

router/middleware.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package router
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"regexp"
8+
"runtime/debug"
9+
"strings"
10+
11+
"github.com/netlify/netlify-commons/tracing"
12+
)
13+
14+
var bearerRegexp = regexp.MustCompile(`^(?:B|b)earer (\S+$)`)
15+
16+
const versionHeaderTempl = "X-NF-%s-Version"
17+
18+
type Middleware func(http.Handler) http.Handler
19+
20+
func MiddlewareFunc(f func(w http.ResponseWriter, r *http.Request, next http.Handler)) Middleware {
21+
return func(next http.Handler) http.Handler {
22+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23+
f(w, r, next)
24+
})
25+
}
26+
}
27+
28+
func VersionHeader(serviceName, version string) Middleware {
29+
return MiddlewareFunc(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
30+
next.ServeHTTP(w, r)
31+
w.Header().Set(fmt.Sprintf(versionHeaderTempl, strings.ToUpper(serviceName)), version)
32+
})
33+
}
34+
35+
func CheckAuth(secret string) Middleware {
36+
return MiddlewareFunc(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
37+
if secret != "" {
38+
authHeader := r.Header.Get("Authorization")
39+
if authHeader == "" {
40+
HandleError(UnauthorizedError("This endpoint requires a Bearer token"), w, r)
41+
}
42+
43+
matches := bearerRegexp.FindStringSubmatch(authHeader)
44+
if len(matches) != 2 {
45+
HandleError(UnauthorizedError("This endpoint requires a Bearer token"), w, r)
46+
}
47+
48+
if secret != matches[1] {
49+
HandleError(UnauthorizedError("This endpoint requires a Bearer token"), w, r)
50+
}
51+
}
52+
53+
next.ServeHTTP(w, r)
54+
})
55+
}
56+
57+
// Recoverer is a middleware that recovers from panics, logs the panic (and a
58+
// backtrace), and returns a HTTP 500 (Internal Server Error) status if
59+
// possible. Recoverer prints a request ID if one is provided.
60+
func Recoverer(w http.ResponseWriter, r *http.Request, next http.Handler) {
61+
defer func() {
62+
if rvr := recover(); rvr != nil {
63+
64+
log := tracing.GetLogger(r)
65+
if log != nil {
66+
log.Errorf("Panic: %+v\n%s", rvr, debug.Stack())
67+
} else {
68+
fmt.Fprintf(os.Stderr, "Panic: %+v\n", rvr)
69+
debug.PrintStack()
70+
}
71+
72+
se := &HTTPError{
73+
Code: http.StatusInternalServerError,
74+
Message: http.StatusText(http.StatusInternalServerError),
75+
}
76+
HandleError(se, w, r)
77+
}
78+
}()
79+
80+
next.ServeHTTP(w, r)
81+
}
82+
83+
func HealthCheck(route string, f APIHandler) Middleware {
84+
return MiddlewareFunc(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
85+
if r.URL.Path == route {
86+
if f == nil {
87+
w.WriteHeader(http.StatusOK)
88+
return
89+
}
90+
HandleError(f(w, r), w, r)
91+
return
92+
}
93+
next.ServeHTTP(w, r)
94+
})
95+
}

router/options.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package router
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
)
6+
7+
type Option func(r *chiWrapper)
8+
9+
func OptEnableCORS(r *chiWrapper) {
10+
r.enableCORS = true
11+
}
12+
13+
func OptHealthCheck(path string, checker APIHandler) Option {
14+
return func(r *chiWrapper) {
15+
r.healthEndpoint = path
16+
r.healthHandler = checker
17+
}
18+
}
19+
20+
func OptVersionHeader(svcName, version string) Option {
21+
return func(r *chiWrapper) {
22+
if version == "" {
23+
version = "unknown"
24+
}
25+
r.version = version
26+
r.svcName = svcName
27+
}
28+
}
29+
30+
func OptTracingMiddleware(log logrus.FieldLogger, svcName string) Option {
31+
return func(r *chiWrapper) {
32+
r.svcName = svcName
33+
r.enableTracing = true
34+
}
35+
}

0 commit comments

Comments
 (0)