@cryptotaxi247 / kubo / commits / a7e50f1fb

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 {