-
Notifications
You must be signed in to change notification settings - Fork 0
/
response.go
278 lines (247 loc) · 8.56 KB
/
response.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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
package treetop
import (
"context"
"errors"
"net/http"
"sync/atomic"
)
var (
token uint32
ErrResponseHijacked = errors.New(
"treetop response: cannot write, HTTP response has been hijacked by another handler")
)
// nextResponseID generates a token which can be used to identify treetop
// responses *locally*. The only uniqueness requirement
// is that concurrent active requests must not possess the same value.
func nextResponseID() uint32 {
return atomic.AddUint32(&token, 1)
}
// Response extends the http.ResponseWriter interface to give ViewHandelersFunc's limited
// ability to control the hierarchical request handling.
//
// Note that writing directly to the underlying ResponseWriter in the handler will cancel the
// treetop handling process. Taking control of response writing in this way is a very common and
// useful practice especially for error messages or redirects.
type Response interface {
http.ResponseWriter
// Status allows a handler to indicate (not determine) what the HTTP status
// should be for the response.
//
// When different handlers indicate a different status,
// the code with the greater numeric value is chosen.
//
// For example, given: Bad Request, Unauthorized and Internal Server Error.
// Status values are differentiated as follows, 400 < 401 < 500,
// 'Internal Server Error' is chosen for the response header.
//
// The resulting response status is returned. Getting the current status
// without affecting the response can be done as follows
//
// status := rsp.Status(0)
//
Status(int) int
// DesignatePageURL forces the response to be handled as a navigation event with a specified URL.
// The browser will have a new history entry created for the supplied URL.
DesignatePageURL(string)
// ReplacePageURL forces the location bar in the web browser to be updated with the supplied
// URL. This should be done by *replacing* the existing history entry. (not adding a new one)
ReplacePageURL(string)
// Finished will return true if a handler has taken direct responsibility for writing the
// response.
Finished() bool
// HandleSubView loads data from a named child subview handler. If no handler is available for the name,
// nil will be returned.
//
// NOTE: Since a sub handler may have returned nil, there is no way for the parent handler to determine
// whether the name resolved to a concrete view.
HandleSubView(string, *http.Request) interface{}
// ResponseID returns the ID treetop has associated with this request.
// Since multiple handlers may be involved, the ID is useful for logging and caching.
//
// Response IDs avoid potential pitfalls around Request instance comparison that can affect middleware.
//
// NOTE: This is *not* a UUID, response IDs are incremented from zero when the server is started
ResponseID() uint32
// Context returns the context associated with the treetop process.
// This is a child of the http Request context.
Context() context.Context
}
// ResponseWrapper is the concrete implementation of the response writer wrapper
// supplied to view handler functions
type ResponseWrapper struct {
http.ResponseWriter
responseID uint32
context context.Context
status int
subViews map[string]*View
pageURL string
pageURLSpecified bool
replaceURL bool
cancel context.CancelFunc
derivedFrom *ResponseWrapper
hijacked bool
}
// BeginResponse initializes the context for a treetop request response
func BeginResponse(cxt context.Context, w http.ResponseWriter) *ResponseWrapper {
rsp := ResponseWrapper{
ResponseWriter: w,
responseID: nextResponseID(),
}
rsp.context, rsp.cancel = context.WithCancel(cxt)
return &rsp
}
// WithSubViews creates a derived response wrapper for a different view, inheriting
// request
func (rsp *ResponseWrapper) WithSubViews(subViews map[string]*View) *ResponseWrapper {
derived := ResponseWrapper{
ResponseWriter: rsp.ResponseWriter,
responseID: rsp.responseID,
subViews: make(map[string]*View),
context: rsp.context,
cancel: rsp.cancel,
derivedFrom: rsp,
hijacked: rsp.hijacked,
}
for k, v := range subViews {
derived.subViews[k] = v
}
return &derived
}
// NewTemplateWriter will return a template Writer configured to add Treetop headers
// based up on the state of the response. If the request is not a template request
// the writer will be nil and the ok flag will be false
func (rsp *ResponseWrapper) NewTemplateWriter(req *http.Request) (Writer, bool) {
if rsp.Finished() {
return nil, false
}
ttW, ok := NewFragmentWriter(rsp.ResponseWriter, req)
if !ok {
return nil, false
}
if rsp.pageURLSpecified {
if rsp.replaceURL {
ttW.ReplacePageURL(rsp.pageURL)
} else {
ttW.DesignatePageURL(rsp.pageURL)
}
}
if rsp.status > 0 {
ttW.Status(rsp.status)
}
return ttW, true
}
// Cancel will teardown the treetop handing process
func (rsp *ResponseWrapper) Cancel() {
rsp.cancel()
}
// markHijacked prevents this response wrapper instance from attempting to
// write to the underlying http.ResponseWriter. This will be called by derived
// response wrappers.
func (rsp *ResponseWrapper) markHijacked() {
if rsp == nil {
return
}
rsp.hijacked = true
// mark all responses to the root handler
rsp.derivedFrom.markHijacked()
}
// Write delegates to the underlying ResponseWriter while aborting the
// treetop executor handler.
func (rsp *ResponseWrapper) Write(b []byte) (int, error) {
if rsp.hijacked {
return 0, ErrResponseHijacked
}
rsp.Cancel()
// prevent parent handler attempting to hijack the response
rsp.derivedFrom.markHijacked()
return rsp.ResponseWriter.Write(b)
}
// WriteHeader delegates to the underlying ResponseWriter while setting finished flag to true
func (rsp *ResponseWrapper) WriteHeader(statusCode int) {
if rsp.Finished() {
// ignore erroneous calls to WriteHeader if the response is finished
return
}
rsp.Cancel()
// prevent parent handler attempting to hijack the response
rsp.derivedFrom.markHijacked()
rsp.ResponseWriter.WriteHeader(statusCode)
}
// Status will set a status for the treetop response headers
// if a response status has been set previously, the larger
// code value will be adopted
func (rsp *ResponseWrapper) Status(status int) int {
if rsp == nil {
return 0
}
if status > rsp.status {
rsp.status = status
}
// propegate status to root handler
rsp.derivedFrom.Status(status)
return rsp.status
}
// ReplacePageURL will instruct the client to replace the current
// history entry with the supplied URL
func (rsp *ResponseWrapper) ReplacePageURL(url string) {
if rsp == nil {
return
}
rsp.pageURL = url
rsp.replaceURL = true
rsp.pageURLSpecified = true
// propegate url to root handler
rsp.derivedFrom.ReplacePageURL(url)
}
// DesignatePageURL will result in a header being added to the response
// that will create a new history entry for the supplied URL
func (rsp *ResponseWrapper) DesignatePageURL(url string) {
if rsp == nil {
return
}
rsp.pageURL = url
rsp.replaceURL = false
rsp.pageURLSpecified = true
// propegate url to root handler
rsp.derivedFrom.DesignatePageURL(url)
}
// Finished will return true if the response headers have been written to the
// client, effectively cancelling the treetop view handler lifecycle
func (rsp *ResponseWrapper) Finished() bool {
if rsp == nil {
return true
}
select {
case <-rsp.context.Done():
return true
default:
return false
}
}
// HandleSubView will execute the handler for a specified sub view of the current view
// if there is no match for the name, nil will be returned.
func (rsp *ResponseWrapper) HandleSubView(name string, req *http.Request) interface{} {
// don't do anything if a response has already been written
if rsp.Finished() || len(rsp.subViews) == 0 {
return nil
}
sub, ok := rsp.subViews[name]
if !ok || sub == nil {
return nil
}
subResp := rsp.WithSubViews(sub.SubViews)
// Invoke sub handler, collecting the response
return sub.HandlerFunc(subResp, req)
}
// Context is getter for the treetop response context which will indicate when the request
// has been completed as was cancelled. This is derived from the request context so
// it can safely be used for cleanup.
func (rsp *ResponseWrapper) Context() context.Context {
return rsp.context
}
// ResponseID is a getter which returns a locally unique ID for a Treetop HTTP response.
// This is intended to be used to keep track of the request as is passes between handlers.
// The ID will increment by one starting at zero, every time the server is restarted.
func (rsp *ResponseWrapper) ResponseID() uint32 {
return rsp.responseID
}