implement http trailers for errors after headers are sent
refactor http handler and copyChunks to get this all to work correctly License: MIT Signed-off-by: Jeromy <jeromyj@gmail.com>
Jeromy committed
Jul 24, 2015 at 17:41 UTC
a7e50f1fbc5a58963a1b1e462575249de61540f1
2 files changed
+64
-41
commands/http/client.go
+32
-4
@@ -3,6 +3,7 @@ package http
3
import (
4
"bytes"
5
"encoding/json"
6
+ "errors"
7
"fmt"
8
"io"
9
"net/http"
@@ -183,16 +184,16 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
184
185
res.SetCloser(httpRes.Body)
186
186
- if len(httpRes.Header.Get(streamHeader)) > 0 {
187
+ if len(httpRes.Header.Get(streamHeader)) > 0 && contentType != "application/json" {
188
// if output is a stream, we can just use the body reader
188
- res.SetOutput(httpRes.Body)
189
+ res.SetOutput(&httpResponseReader{httpRes})
190
return res, nil
191
192
} else if len(httpRes.Header.Get(channelHeader)) > 0 {
193
// if output is coming from a channel, decode each chunk
194
outChan := make(chan interface{})
195
go func() {
195
- dec := json.NewDecoder(httpRes.Body)
196
+ dec := json.NewDecoder(&httpResponseReader{httpRes})
197
outputType := reflect.TypeOf(req.Command().Type)
198
199
ctx := req.Context()
@@ -237,7 +238,7 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
238
return res, nil
239
}
240
240
- dec := json.NewDecoder(httpRes.Body)
241
+ dec := json.NewDecoder(&httpResponseReader{httpRes})
242
243
if httpRes.StatusCode >= http.StatusBadRequest {
244
e := cmds.Error{}
@@ -284,3 +285,30 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
285
286
return res, nil
287
}
288
+
289
+type httpResponseReader struct {
290
+ resp *http.Response
291
+}
292
+
293
+func (r *httpResponseReader) Read(b []byte) (int, error) {
294
+ n, err := r.resp.Body.Read(b)
295
+ if err == io.EOF {
296
+ _ = r.resp.Body.Close()
297
+ trailerErr := r.checkError()
298
+ if trailerErr != nil {
299
+ return n, trailerErr
300
+ }
301
+ }
302
+ return n, err
303
+}
304
+
305
+func (r *httpResponseReader) checkError() error {
306
+ if e := r.resp.Trailer.Get(StreamErrHeader); e != "" {
307
+ return errors.New(e)
308
+ }
309
+ return nil
310
+}
311
+
312
+func (r *httpResponseReader) Close() error {
313
+ return r.resp.Body.Close()
314
+}
commands/http/handler.go
+32
-37
@@ -118,43 +118,43 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
118
// call the command
119
res := i.root.Call(req)
120
121
- // set the Content-Type based on res output
121
+ // now handle responding to the client properly
122
+ sendResponse(w, req, res)
123
+}
124
+
125
+func sendResponse(w http.ResponseWriter, req cmds.Request, res cmds.Response) {
126
+
127
+ var mime string
128
if _, ok := res.Output().(io.Reader); ok {
129
+ mime = ""
130
// we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves
131
// we set this header so clients have a way to know this is an output stream
132
// (not marshalled command output)
133
// TODO: set a specific Content-Type if the command response needs it to be a certain type
127
- w.Header().Set(streamHeader, "1")
128
-
134
} else {
130
- enc, found, err := req.Option(cmds.EncShort).String()
135
+ // Try to guess mimeType from the encoding option
136
+ enc, found, err := res.Request().Option(cmds.EncShort).String()
137
if err != nil || !found {
138
w.WriteHeader(http.StatusInternalServerError)
139
return
140
}
135
- mime := mimeTypes[enc]
136
- w.Header().Set(contentTypeHeader, mime)
137
- }
138
-
139
- // set the Content-Length from the response length
140
- if res.Length() > 0 {
141
- w.Header().Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
141
+ mime = mimeTypes[enc]
142
}
143
144
+ status := 200
145
// if response contains an error, write an HTTP error status code
146
if e := res.Error(); e != nil {
147
if e.Code == cmds.ErrClient {
147
- w.WriteHeader(http.StatusBadRequest)
148
+ status = http.StatusBadRequest
149
} else {
149
- w.WriteHeader(http.StatusInternalServerError)
150
+ status = http.StatusInternalServerError
151
}
152
+ // TODO: do we just ignore this error? or what?
153
}
154
155
out, err := res.Reader()
156
if err != nil {
155
- w.Header().Set(contentTypeHeader, "text/plain")
156
- w.WriteHeader(http.StatusInternalServerError)
157
- w.Write([]byte(err.Error()))
157
+ http.Error(w, err.Error(), http.StatusInternalServerError)
158
return
159
}
160
@@ -167,13 +167,11 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
167
168
streamChans, _, _ := req.Option("stream-channels").Bool()
169
if isChan && streamChans {
170
- if err := copyChunks(applicationJson, w, out); err != nil {
171
- log.Error("error while writing stream", err)
172
- }
173
- return
170
+ // streaming output from a channel will always be json objects
171
+ mime = applicationJson
172
}
173
176
- if err := flushCopy(w, out); err != nil {
174
+ if err := copyChunks(mime, status, isChan, res.Length(), w, out); err != nil {
175
log.Error("error while writing stream", err)
176
}
177
}
@@ -183,20 +181,9 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
181
i.corsHandler.ServeHTTP(w, r)
182
}
183
186
-// flushCopy Copies from an io.Reader to a http.ResponseWriter.
187
-// Flushes chunks over HTTP stream as they are read (if supported by transport).
188
-func flushCopy(w http.ResponseWriter, out io.Reader) error {
189
- if _, ok := w.(http.Flusher); !ok {
190
- return copyChunks("", w, out)
191
- }
192
-
193
- _, err := io.Copy(&flushResponse{w}, out)
194
- return err
195
-}
196
-
184
// Copies from an io.Reader to a http.ResponseWriter.
185
// Flushes chunks over HTTP stream as they are read (if supported by transport).
199
-func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error {
186
+func copyChunks(contentType string, status int, channel bool, length uint64, w http.ResponseWriter, out io.Reader) error {
187
hijacker, ok := w.(http.Hijacker)
188
if !ok {
189
return errors.New("Could not create hijacker")
@@ -207,12 +194,20 @@ func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error
194
}
195
defer conn.Close()
196
210
- writer.WriteString("HTTP/1.1 200 OK\r\n")
197
+ writer.WriteString(fmt.Sprintf("HTTP/1.1 %d %s\r\n", status, http.StatusText(status)))
198
+ writer.WriteString(streamHeader + ": 1\r\n")
199
if contentType != "" {
200
writer.WriteString(contentTypeHeader + ": " + contentType + "\r\n")
201
}
202
+ if channel {
203
+ writer.WriteString(channelHeader + ": 1\r\n")
204
+ }
205
+ if length > 0 {
206
+ w.Header().Set(contentLengthHeader, strconv.FormatUint(length, 10))
207
+ }
208
writer.WriteString(transferEncodingHeader + ": chunked\r\n")
215
- writer.WriteString(channelHeader + ": 1\r\n\r\n")
209
+
210
+ writer.WriteString("\r\n")
211
212
writeChunks := func() error {
213
buf := make([]byte, 32*1024)
@@ -248,11 +243,11 @@ func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error
243
// if there was a stream error, write out an error trailer. hopefully
244
// the client will pick it up!
245
if streamErr != nil {
251
- writer.WriteString(StreamErrHeader + ": " + sanitizedErrStr(err) + "\r\n")
246
+ writer.WriteString(StreamErrHeader + ": " + sanitizedErrStr(streamErr) + "\r\n")
247
}
248
writer.WriteString("\r\n") // close response
249
writer.Flush()
255
- return nil
250
+ return streamErr
251
}
252
253
func sanitizedErrStr(err error) string {