1
1
package http
2
2
3
3
import (
4
+ "bufio"
4
5
"errors"
5
6
"fmt"
6
7
"io"
@@ -32,12 +33,17 @@ type Handler struct {
32
33
var ErrNotFound = errors .New ("404 page not found" )
33
34
34
35
const (
36
+ StreamErrHeader = "X-Stream-Error"
35
37
streamHeader = "X-Stream-Output"
36
38
channelHeader = "X-Chunked-Output"
39
+ uaHeader = "User-Agent"
37
40
contentTypeHeader = "Content-Type"
38
41
contentLengthHeader = "Content-Length"
42
+ contentDispHeader = "Content-Disposition"
39
43
transferEncodingHeader = "Transfer-Encoding"
40
44
applicationJson = "application/json"
45
+ applicationOctetStream = "application/octet-stream"
46
+ plainText = "text/plain"
41
47
)
42
48
43
49
var mimeTypes = map [string ]string {
@@ -70,6 +76,11 @@ func NewHandler(ctx cmds.Context, root *cmds.Command, allowedOrigin string) *Han
70
76
return & Handler {internal , c .Handler (internal )}
71
77
}
72
78
79
+ func (i Handler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
80
+ // Call the CORS handler which wraps the internal handler.
81
+ i .corsHandler .ServeHTTP (w , r )
82
+ }
83
+
73
84
func (i internalHandler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
74
85
log .Debug ("Incoming API request: " , r .URL )
75
86
@@ -101,8 +112,8 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
101
112
// get the node's context to pass into the commands.
102
113
node , err := i .ctx .GetNode ()
103
114
if err != nil {
104
- err = fmt .Errorf ("cmds/http: couldn't GetNode(): %s" , err )
105
- http .Error (w , err . Error () , http .StatusInternalServerError )
115
+ s : = fmt .Sprintf ("cmds/http: couldn't GetNode(): %s" , err )
116
+ http .Error (w , s , http .StatusInternalServerError )
106
117
return
107
118
}
108
119
@@ -117,46 +128,60 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
117
128
// call the command
118
129
res := i .root .Call (req )
119
130
120
- // set the Content-Type based on res output
131
+ // now handle responding to the client properly
132
+ sendResponse (w , req , res )
133
+ }
134
+
135
+ func guessMimeType (res cmds.Response ) (string , error ) {
121
136
if _ , ok := res .Output ().(io.Reader ); ok {
122
137
// we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves
123
138
// we set this header so clients have a way to know this is an output stream
124
139
// (not marshalled command output)
125
140
// TODO: set a specific Content-Type if the command response needs it to be a certain type
126
- w .Header ().Set (streamHeader , "1" )
141
+ return "" , nil
142
+ }
127
143
128
- } else {
129
- enc , found , err := req .Option (cmds .EncShort ).String ()
130
- if err != nil || ! found {
131
- w .WriteHeader (http .StatusInternalServerError )
132
- return
133
- }
134
- mime := mimeTypes [enc ]
135
- w .Header ().Set (contentTypeHeader , mime )
144
+ // Try to guess mimeType from the encoding option
145
+ enc , found , err := res .Request ().Option (cmds .EncShort ).String ()
146
+ if err != nil {
147
+ return "" , err
148
+ }
149
+ if ! found {
150
+ return "" , errors .New ("no encoding option set" )
136
151
}
137
152
138
- // set the Content-Length from the response length
139
- if res .Length () > 0 {
140
- w .Header ().Set (contentLengthHeader , strconv .FormatUint (res .Length (), 10 ))
153
+ return mimeTypes [enc ], nil
154
+ }
155
+
156
+ func sendResponse (w http.ResponseWriter , req cmds.Request , res cmds.Response ) {
157
+ mime , err := guessMimeType (res )
158
+ if err != nil {
159
+ http .Error (w , err .Error (), http .StatusInternalServerError )
160
+ return
141
161
}
142
162
163
+ status := http .StatusOK
143
164
// if response contains an error, write an HTTP error status code
144
165
if e := res .Error (); e != nil {
145
166
if e .Code == cmds .ErrClient {
146
- w . WriteHeader ( http .StatusBadRequest )
167
+ status = http .StatusBadRequest
147
168
} else {
148
- w . WriteHeader ( http .StatusInternalServerError )
169
+ status = http .StatusInternalServerError
149
170
}
171
+ // NOTE: The error will actually be written out by the reader below
150
172
}
151
173
152
174
out , err := res .Reader ()
153
175
if err != nil {
154
- w .Header ().Set (contentTypeHeader , "text/plain" )
155
- w .WriteHeader (http .StatusInternalServerError )
156
- w .Write ([]byte (err .Error ()))
176
+ http .Error (w , err .Error (), http .StatusInternalServerError )
157
177
return
158
178
}
159
179
180
+ h := w .Header ()
181
+ if res .Length () > 0 {
182
+ h .Set (contentLengthHeader , strconv .FormatUint (res .Length (), 10 ))
183
+ }
184
+
160
185
// if output is a channel and user requested streaming channels,
161
186
// use chunk copier for the output
162
187
_ , isChan := res .Output ().(chan interface {})
@@ -165,44 +190,32 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
165
190
}
166
191
167
192
streamChans , _ , _ := req .Option ("stream-channels" ).Bool ()
168
- if isChan && streamChans {
169
- // w.WriteString(transferEncodingHeader + ": chunked\r\n")
170
- // w.Header().Set(channelHeader, "1")
171
- // w.WriteHeader(200)
172
- err = copyChunks (applicationJson , w , out )
173
- if err != nil {
174
- log .Debug ("copy chunks error: " , err )
193
+ if isChan {
194
+ h .Set (channelHeader , "1" )
195
+ if streamChans {
196
+ // streaming output from a channel will always be json objects
197
+ mime = applicationJson
175
198
}
176
- return
177
199
}
178
200
179
- err = flushCopy (w , out )
180
- if err != nil {
181
- log .Debug ("Flush copy returned an error: " , err )
201
+ if mime != "" {
202
+ h .Set (contentTypeHeader , mime )
182
203
}
183
- }
204
+ h .Set (streamHeader , "1" )
205
+ h .Set (transferEncodingHeader , "chunked" )
184
206
185
- func (i Handler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
186
- // Call the CORS handler which wraps the internal handler.
187
- i .corsHandler .ServeHTTP (w , r )
188
- }
189
-
190
- // flushCopy Copies from an io.Reader to a http.ResponseWriter.
191
- // Flushes chunks over HTTP stream as they are read (if supported by transport).
192
- func flushCopy (w http.ResponseWriter , out io.Reader ) error {
193
- if _ , ok := w .(http.Flusher ); ! ok {
194
- return copyChunks ("" , w , out )
207
+ if err := writeResponse (status , w , out ); err != nil {
208
+ log .Error ("error while writing stream" , err )
195
209
}
196
-
197
- _ , err := io .Copy (& flushResponse {w }, out )
198
- return err
199
210
}
200
211
201
212
// Copies from an io.Reader to a http.ResponseWriter.
202
213
// Flushes chunks over HTTP stream as they are read (if supported by transport).
203
- func copyChunks (contentType string , w http.ResponseWriter , out io.Reader ) error {
214
+ func writeResponse (status int , w http.ResponseWriter , out io.Reader ) error {
215
+ // hijack the connection so we can write our own chunked output and trailers
204
216
hijacker , ok := w .(http.Hijacker )
205
217
if ! ok {
218
+ log .Error ("Failed to create hijacker! cannot continue!" )
206
219
return errors .New ("Could not create hijacker" )
207
220
}
208
221
conn , writer , err := hijacker .Hijack ()
@@ -211,29 +224,47 @@ func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error
211
224
}
212
225
defer conn .Close ()
213
226
214
- writer .WriteString ("HTTP/1.1 200 OK\r \n " )
215
- if contentType != "" {
216
- writer .WriteString (contentTypeHeader + ": " + contentType + "\r \n " )
227
+ // write status
228
+ writer .WriteString (fmt .Sprintf ("HTTP/1.1 %d %s\r \n " , status , http .StatusText (status )))
229
+
230
+ // Write out headers
231
+ w .Header ().Write (writer )
232
+
233
+ // end of headers
234
+ writer .WriteString ("\r \n " )
235
+
236
+ // write body
237
+ streamErr := writeChunks (out , writer )
238
+
239
+ // close body
240
+ writer .WriteString ("0\r \n " )
241
+
242
+ // if there was a stream error, write out an error trailer. hopefully
243
+ // the client will pick it up!
244
+ if streamErr != nil {
245
+ writer .WriteString (StreamErrHeader + ": " + sanitizedErrStr (streamErr ) + "\r \n " )
217
246
}
218
- writer .WriteString (transferEncodingHeader + ": chunked\r \n " )
219
- writer .WriteString (channelHeader + ": 1\r \n \r \n " )
247
+ writer .WriteString ("\r \n " ) // close response
248
+ writer .Flush ()
249
+ return streamErr
250
+ }
220
251
252
+ func writeChunks (r io.Reader , w * bufio.ReadWriter ) error {
221
253
buf := make ([]byte , 32 * 1024 )
222
-
223
254
for {
224
- n , err := out .Read (buf )
255
+ n , err := r .Read (buf )
225
256
226
257
if n > 0 {
227
258
length := fmt .Sprintf ("%x\r \n " , n )
228
- writer .WriteString (length )
259
+ w .WriteString (length )
229
260
230
- _ , err := writer .Write (buf [0 :n ])
261
+ _ , err := w .Write (buf [0 :n ])
231
262
if err != nil {
232
263
return err
233
264
}
234
265
235
- writer .WriteString ("\r \n " )
236
- writer .Flush ()
266
+ w .WriteString ("\r \n " )
267
+ w .Flush ()
237
268
}
238
269
239
270
if err != nil && err != io .EOF {
@@ -243,25 +274,12 @@ func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error
243
274
break
244
275
}
245
276
}
246
-
247
- writer .WriteString ("0\r \n \r \n " )
248
- writer .Flush ()
249
-
250
277
return nil
251
278
}
252
279
253
- type flushResponse struct {
254
- W http.ResponseWriter
255
- }
256
-
257
- func (fr * flushResponse ) Write (buf []byte ) (int , error ) {
258
- n , err := fr .W .Write (buf )
259
- if err != nil {
260
- return n , err
261
- }
262
-
263
- if flusher , ok := fr .W .(http.Flusher ); ok {
264
- flusher .Flush ()
265
- }
266
- return n , err
280
+ func sanitizedErrStr (err error ) string {
281
+ s := err .Error ()
282
+ s = strings .Split (s , "\n " )[0 ]
283
+ s = strings .Split (s , "\r " )[0 ]
284
+ return s
267
285
}
0 commit comments