Skip to content

Commit f84c071

Browse files
authored
Add ValidationError type (#22)
When a payload fails validation, due to missing headers, an invalid signature, or other reasons, create a ValidationError so that error handlers can identify this case. Modify the default handler to return a 400 error in this case instead of a 500 or 202 response.
1 parent 98335f1 commit f84c071

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

githubapp/dispatcher.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package githubapp
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"net/http"
2021

2122
"github.com/google/go-github/github"
@@ -71,6 +72,18 @@ func WithResponseCallback(onResponse ResponseCallback) DispatcherOption {
7172
}
7273
}
7374

75+
// ValidationError is passed to error callbacks when the webhook payload fails
76+
// validation.
77+
type ValidationError struct {
78+
EventType string
79+
DeliveryID string
80+
Cause error
81+
}
82+
83+
func (ve ValidationError) Error() string {
84+
return fmt.Sprintf("invalid event: %v", ve.Cause)
85+
}
86+
7487
type eventDispatcher struct {
7588
handlerMap map[string]EventHandler
7689
secret string
@@ -131,8 +144,11 @@ func (d *eventDispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
131144
deliveryID := r.Header.Get("X-GitHub-Delivery")
132145

133146
if eventType == "" {
134-
// ACK payload that was received but won't be processed
135-
w.WriteHeader(http.StatusAccepted)
147+
d.onError(w, r, ValidationError{
148+
EventType: eventType,
149+
DeliveryID: deliveryID,
150+
Cause: errors.New("missing event type"),
151+
})
136152
return
137153
}
138154

@@ -147,7 +163,11 @@ func (d *eventDispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
147163

148164
payloadBytes, err := github.ValidatePayload(r, []byte(d.secret))
149165
if err != nil {
150-
d.onError(w, r, errors.Wrapf(err, "failed to validate webhook payload"))
166+
d.onError(w, r, ValidationError{
167+
EventType: eventType,
168+
DeliveryID: deliveryID,
169+
Cause: err,
170+
})
151171
return
152172
}
153173

@@ -166,6 +186,13 @@ func (d *eventDispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
166186
// DefaultErrorCallback logs errors and responds with a 500 status code.
167187
func DefaultErrorCallback(w http.ResponseWriter, r *http.Request, err error) {
168188
logger := zerolog.Ctx(r.Context())
189+
190+
if ve, ok := err.(ValidationError); ok {
191+
logger.Warn().Err(ve.Cause).Msgf("Received invalid webhook headers or payload")
192+
http.Error(w, "Invalid webhook headers or payload", http.StatusBadRequest)
193+
return
194+
}
195+
169196
logger.Error().Err(err).Msg("Unexpected error handling webhook request")
170197
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
171198
}

githubapp/dispatcher_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func TestEventDispatcher(t *testing.T) {
3838
Handler TestEventHandler
3939
Options []DispatcherOption
4040
Event string
41+
Invalid bool
4142

4243
ResponseCode int
4344
ResponseBody string
@@ -65,7 +66,7 @@ func TestEventDispatcher(t *testing.T) {
6566
ResponseCode: 200,
6667
CallCount: 1,
6768
},
68-
"defaultErrorHandlerReturns500": {
69+
"defaultErrorHandlerReturns500OnError": {
6970
Handler: TestEventHandler{
7071
Types: []string{"pull_request"},
7172
Fn: func(ctx context.Context, eventType, deliveryID string, payload []byte) error {
@@ -77,6 +78,15 @@ func TestEventDispatcher(t *testing.T) {
7778
ResponseBody: "Internal Server Error\n",
7879
CallCount: 1,
7980
},
81+
"defaultErrorHandlerReturns400OnInvalid": {
82+
Handler: TestEventHandler{
83+
Types: []string{"pull_request"},
84+
},
85+
Event: "pull_request",
86+
Invalid: true,
87+
ResponseCode: 400,
88+
ResponseBody: "Invalid webhook headers or payload\n",
89+
},
8090
"callsCustomErrorCallback": {
8191
Handler: TestEventHandler{
8292
Types: []string{"pull_request"},
@@ -151,7 +161,7 @@ func TestEventDispatcher(t *testing.T) {
151161
h := test.Handler
152162
d := NewEventDispatcher([]EventHandler{&h}, testHookSecret, test.Options...)
153163

154-
req := newSignedRequest(test.Event, name)
164+
req := newHookRequest(test.Event, name, !test.Invalid)
155165
res := httptest.NewRecorder()
156166
d.ServeHTTP(res, req)
157167

@@ -181,17 +191,19 @@ func TestSetAndGetResponder(t *testing.T) {
181191
})
182192
}
183193

184-
func newSignedRequest(eventType, id string) *http.Request {
194+
func newHookRequest(eventType, id string, signed bool) *http.Request {
185195
body := []byte(fmt.Sprintf(`{"type":"%s"}`, eventType))
186196

187197
req := httptest.NewRequest(http.MethodPost, "/api/github/hook", bytes.NewReader(body))
188198
req.Header.Set("Content-Type", "application/json")
189199
req.Header.Set("X-Github-Event", eventType)
190200
req.Header.Set("X-Github-Delivery", id)
191201

192-
mac := hmac.New(sha1.New, []byte(testHookSecret))
193-
mac.Write(body)
194-
req.Header.Set("X-Hub-Signature", fmt.Sprintf("sha1=%x", mac.Sum(nil)))
202+
if signed {
203+
mac := hmac.New(sha1.New, []byte(testHookSecret))
204+
mac.Write(body)
205+
req.Header.Set("X-Hub-Signature", fmt.Sprintf("sha1=%x", mac.Sum(nil)))
206+
}
195207

196208
log := zerolog.New(os.Stdout)
197209
req = req.WithContext(log.WithContext(req.Context()))

0 commit comments

Comments
 (0)