@@ -41,16 +41,47 @@ type EventHandler interface {
41
41
Handle (ctx context.Context , eventType , deliveryID string , payload []byte ) error
42
42
}
43
43
44
- type ErrorHandler func (http.ResponseWriter , * http.Request , error )
44
+ // ErrorCallback is called when an event handler returns an error. The error
45
+ // from the handler is passed directly as the final argument.
46
+ type ErrorCallback func (w http.ResponseWriter , r * http.Request , err error )
47
+
48
+ // ResponseCallback is called to send a response to GitHub after an event is
49
+ // handled. It is passed the event type and a flag indicating if an event
50
+ // handler was called for the event.
51
+ type ResponseCallback func (w http.ResponseWriter , r * http.Request , event string , handled bool )
52
+
53
+ // DispatcherOption configures properties of an event dispatcher.
54
+ type DispatcherOption func (* eventDispatcher )
55
+
56
+ // WithErrorCallback sets the error callback for an event dispatcher.
57
+ func WithErrorCallback (onError ErrorCallback ) DispatcherOption {
58
+ return func (d * eventDispatcher ) {
59
+ if onError != nil {
60
+ d .onError = onError
61
+ }
62
+ }
63
+ }
64
+
65
+ // WithResponseCallback sets the response callback for an event dispatcher.
66
+ func WithResponseCallback (onResponse ResponseCallback ) DispatcherOption {
67
+ return func (d * eventDispatcher ) {
68
+ if onResponse != nil {
69
+ d .onResponse = onResponse
70
+ }
71
+ }
72
+ }
45
73
46
74
type eventDispatcher struct {
47
75
handlerMap map [string ]EventHandler
48
76
secret string
49
- onError ErrorHandler
77
+
78
+ onError ErrorCallback
79
+ onResponse ResponseCallback
50
80
}
51
81
52
- // NewDefaultEventDispatcher is a convenience method to create an
53
- // EventDispatcher from configuration using the default error handler.
82
+ // NewDefaultEventDispatcher is a convenience method to create an event
83
+ // dispatcher from configuration using the default error and response
84
+ // callbacks.
54
85
func NewDefaultEventDispatcher (c Config , handlers ... EventHandler ) http.Handler {
55
86
return NewEventDispatcher (handlers , c .App .WebhookSecret , nil )
56
87
}
@@ -59,10 +90,9 @@ func NewDefaultEventDispatcher(c Config, handlers ...EventHandler) http.Handler
59
90
// requests to the appropriate event handlers. It validates payload integrity
60
91
// using the given secret value.
61
92
//
62
- // If an error occurs during handling, the error handler is called with the
63
- // error and should write an appropriate response. If the error handler is nil,
64
- // a default handler is used.
65
- func NewEventDispatcher (handlers []EventHandler , secret string , onError ErrorHandler ) http.Handler {
93
+ // Responses are controlled by optional error and response callbacks. If these
94
+ // options are not provided, default callbacks are used.
95
+ func NewEventDispatcher (handlers []EventHandler , secret string , opts ... DispatcherOption ) http.Handler {
66
96
handlerMap := make (map [string ]EventHandler )
67
97
68
98
// Iterate in reverse so the first entries in the slice have priority
@@ -72,35 +102,46 @@ func NewEventDispatcher(handlers []EventHandler, secret string, onError ErrorHan
72
102
}
73
103
}
74
104
75
- if onError == nil {
76
- onError = DefaultErrorHandler
77
- }
78
-
79
- return & eventDispatcher {
105
+ d := & eventDispatcher {
80
106
handlerMap : handlerMap ,
81
107
secret : secret ,
82
- onError : onError ,
108
+ onError : DefaultErrorCallback ,
109
+ onResponse : DefaultResponseCallback ,
83
110
}
111
+
112
+ for _ , opt := range opts {
113
+ opt (d )
114
+ }
115
+
116
+ return d
84
117
}
85
118
86
- // ServeHTTP to implement http.Handler
119
+ // ServeHTTP processes a webhook request from GitHub.
87
120
func (d * eventDispatcher ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
88
121
ctx := r .Context ()
89
122
123
+ // initialize context for SetResponder/GetResponder
124
+ // we store a pointer in the context so that functions deeper in the call
125
+ // tree can modify the value without creating a new context
126
+ var responder func (http.ResponseWriter , * http.Request )
127
+ ctx = context .WithValue (ctx , responderKey {}, & responder )
128
+ r = r .WithContext (ctx )
129
+
90
130
eventType := r .Header .Get ("X-GitHub-Event" )
131
+ deliveryID := r .Header .Get ("X-GitHub-Delivery" )
132
+
91
133
if eventType == "" {
92
134
// ACK payload that was received but won't be processed
93
135
w .WriteHeader (http .StatusAccepted )
94
136
return
95
137
}
96
- deliveryID := r .Header .Get ("X-GitHub-Delivery" )
97
138
98
139
logger := zerolog .Ctx (ctx ).With ().
99
140
Str (LogKeyEventType , eventType ).
100
141
Str (LogKeyDeliveryID , deliveryID ).
101
142
Logger ()
102
143
103
- // update context and request to contain new log fields
144
+ // initialize context with event logger
104
145
ctx = logger .WithContext (ctx )
105
146
r = r .WithContext (ctx )
106
147
@@ -111,28 +152,65 @@ func (d *eventDispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
111
152
}
112
153
113
154
logger .Info ().Msgf ("Received webhook event" )
114
- handler , ok := d .handlerMap [eventType ]
115
155
116
- switch {
117
- case ok :
156
+ handler , ok := d . handlerMap [ eventType ]
157
+ if ok {
118
158
if err := handler .Handle (ctx , eventType , deliveryID , payloadBytes ); err != nil {
119
- // pass error directly so handler can inspect types if needed
120
159
d .onError (w , r , err )
121
160
return
122
161
}
123
- w .WriteHeader (http .StatusOK )
124
- case eventType == "ping" :
125
- w .WriteHeader (http .StatusOK )
126
- default :
127
- w .WriteHeader (http .StatusAccepted )
128
162
}
163
+ d .onResponse (w , r , eventType , ok )
129
164
}
130
165
131
- // DefaultErrorHandler logs errors and responds with a 500 status code.
132
- func DefaultErrorHandler (w http.ResponseWriter , r * http.Request , err error ) {
166
+ // DefaultErrorCallback logs errors and responds with a 500 status code.
167
+ func DefaultErrorCallback (w http.ResponseWriter , r * http.Request , err error ) {
133
168
logger := zerolog .Ctx (r .Context ())
134
169
logger .Error ().Err (err ).Msg ("Unexpected error handling webhook request" )
170
+ http .Error (w , http .StatusText (http .StatusInternalServerError ), http .StatusInternalServerError )
171
+ }
135
172
136
- msg := http .StatusText (http .StatusInternalServerError )
137
- http .Error (w , msg , http .StatusInternalServerError )
173
+ // DefaultResponseCallback responds with a 200 OK for handled events and a 202
174
+ // Accepted status for all other events. By default, responses are empty.
175
+ // Event handlers may send custom responses by calling the SetResponder
176
+ // function before returning.
177
+ func DefaultResponseCallback (w http.ResponseWriter , r * http.Request , event string , handled bool ) {
178
+ if ! handled && event != "ping" {
179
+ w .WriteHeader (http .StatusAccepted )
180
+ return
181
+ }
182
+
183
+ if res := GetResponder (r .Context ()); res != nil {
184
+ res (w , r )
185
+ } else {
186
+ w .WriteHeader (http .StatusOK )
187
+ }
188
+ }
189
+
190
+ type responderKey struct {}
191
+
192
+ // SetResponder sets a function that sends a response to GitHub after event
193
+ // processing completes. This function may only be called from event handler
194
+ // functions invoked by the event dispatcher.
195
+ //
196
+ // Customizing individual handler responses should be rare. Applications that
197
+ // want to modify the standard responses should consider registering a response
198
+ // callback before using this function.
199
+ func SetResponder (ctx context.Context , responder func (http.ResponseWriter , * http.Request )) {
200
+ r , ok := ctx .Value (responderKey {}).(* func (http.ResponseWriter , * http.Request ))
201
+ if ! ok || r == nil {
202
+ panic ("SetResponder() must be called from an event handler invoked by the go-githubapp event dispatcher" )
203
+ }
204
+ * r = responder
205
+ }
206
+
207
+ // GetResponder returns the response function that was set by an event handler.
208
+ // If no response function exists, it returns nil. There is usually no reason
209
+ // to call this outside of a response callback implementation.
210
+ func GetResponder (ctx context.Context ) func (http.ResponseWriter , * http.Request ) {
211
+ r , ok := ctx .Value (responderKey {}).(* func (http.ResponseWriter , * http.Request ))
212
+ if ! ok || r == nil {
213
+ return nil
214
+ }
215
+ return * r
138
216
}
0 commit comments