Skip to content

Commit e488a34

Browse files
author
keks
committed
http: send errors as trailer iff status is 200, else as values
1 parent 029db85 commit e488a34

File tree

5 files changed

+181
-45
lines changed

5 files changed

+181
-45
lines changed

http/client.go

+28-16
Original file line numberDiff line numberDiff line change
@@ -123,21 +123,7 @@ func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.En
123123
return cmds.Copy(re, res)
124124
}
125125

126-
func (c *client) Send(req *cmds.Request) (cmds.Response, error) {
127-
if req.Context == nil {
128-
log.Warningf("no context set in request")
129-
req.Context = context.Background()
130-
}
131-
132-
// save user-provided encoding
133-
previousUserProvidedEncoding, found := req.Options[cmds.EncLong].(string)
134-
135-
// override with json to send to server
136-
req.SetOption(cmds.EncLong, cmds.JSON)
137-
138-
// stream channel output
139-
req.SetOption(cmds.ChanOpt, true)
140-
126+
func (c *client) toHTTPRequest(req *cmds.Request) (*http.Request, error) {
141127
query, err := getQuery(req)
142128
if err != nil {
143129
return nil, err
@@ -176,17 +162,43 @@ func (c *client) Send(req *cmds.Request) (cmds.Response, error) {
176162
httpReq = httpReq.WithContext(req.Context)
177163
httpReq.Close = true
178164

165+
return httpReq, nil
166+
}
167+
168+
func (c *client) Send(req *cmds.Request) (cmds.Response, error) {
169+
if req.Context == nil {
170+
log.Warningf("no context set in request")
171+
req.Context = context.Background()
172+
}
173+
174+
// save user-provided encoding
175+
previousUserProvidedEncoding, found := req.Options[cmds.EncLong].(string)
176+
177+
// override with json to send to server
178+
req.SetOption(cmds.EncLong, cmds.JSON)
179+
180+
// stream channel output
181+
req.SetOption(cmds.ChanOpt, true)
182+
183+
// build http request
184+
httpReq, err := c.toHTTPRequest(req)
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
// send http request
179190
httpRes, err := c.httpClient.Do(httpReq)
180191
if err != nil {
181192
return nil, err
182193
}
183194

184-
// using the overridden JSON encoding in request
195+
// parse using the overridden JSON encoding in request
185196
res, err := parseResponse(httpRes, req)
186197
if err != nil {
187198
return nil, err
188199
}
189200

201+
// reset request encoding to what it was before
190202
if found && len(previousUserProvidedEncoding) > 0 {
191203
// reset to user provided encoding after sending request
192204
// NB: if user has provided an encoding but it is the empty string,

http/errors_test.go

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io/ioutil"
7+
"net/http"
8+
"runtime"
9+
"strings"
10+
"testing"
11+
12+
"github.com/ipfs/go-ipfs-cmds"
13+
)
14+
15+
func TestErrors(t *testing.T) {
16+
type testcase struct {
17+
path []string
18+
bodyStr string
19+
status string
20+
errTrailer string
21+
}
22+
23+
tcs := []testcase{
24+
{
25+
path: []string{"version"},
26+
bodyStr: `{` +
27+
`"Version":"0.1.2",` +
28+
`"Commit":"c0mm17",` +
29+
`"Repo":"4",` +
30+
`"System":"` + runtime.GOARCH + "/" + runtime.GOOS + `",` +
31+
`"Golang":"` + runtime.Version() + `"}` + "\n",
32+
status: "200 OK",
33+
},
34+
35+
// TODO this error should be sent as a value, because it is non-200
36+
{
37+
path: []string{"error"},
38+
status: "500 Internal Server Error",
39+
bodyStr: `{"Message":"an error occurred","Code":0,"Type":"error"}` + "\n",
40+
},
41+
42+
{
43+
path: []string{"lateerror"},
44+
status: "200 OK",
45+
bodyStr: `"some value"` + "\n",
46+
errTrailer: "an error occurred",
47+
},
48+
49+
{
50+
path: []string{"doubleclose"},
51+
status: "200 OK",
52+
bodyStr: `"some value"` + "\n",
53+
},
54+
55+
{
56+
path: []string{"single"},
57+
status: "200 OK",
58+
bodyStr: `"some value"` + "\n",
59+
},
60+
61+
{
62+
path: []string{"reader"},
63+
status: "200 OK",
64+
bodyStr: "the reader call returns a reader.",
65+
},
66+
}
67+
68+
mkTest := func(tc testcase) func(*testing.T) {
69+
return func(t *testing.T) {
70+
_, srv := getTestServer(t, nil) // handler_test:/^func getTestServer/
71+
c := NewClient(srv.URL)
72+
req, err := cmds.NewRequest(context.Background(), tc.path, nil, nil, nil, cmdRoot)
73+
if err != nil {
74+
t.Fatal(err)
75+
}
76+
77+
httpReq, err := c.(*client).toHTTPRequest(req)
78+
if err != nil {
79+
t.Fatal("unexpected error:", err)
80+
}
81+
82+
httpClient := http.DefaultClient
83+
84+
res, err := httpClient.Do(httpReq)
85+
if err != nil {
86+
t.Fatal("unexpected error", err)
87+
}
88+
89+
if res.Status != tc.status {
90+
t.Errorf("expected status %v, got %v", tc.status, res.Status)
91+
}
92+
93+
body, err := ioutil.ReadAll(res.Body)
94+
if err != nil {
95+
t.Fatal("err reading response body", err)
96+
}
97+
98+
if bodyStr := string(body); bodyStr != tc.bodyStr {
99+
t.Errorf("expected body string \n\n%v\n\n, got\n\n%v", tc.bodyStr, bodyStr)
100+
}
101+
102+
if errTrailer := res.Trailer.Get(StreamErrHeader); errTrailer != tc.errTrailer {
103+
t.Errorf("expected error header %q, got %q", tc.errTrailer, errTrailer)
104+
}
105+
}
106+
}
107+
108+
for i, tc := range tcs {
109+
t.Run(fmt.Sprintf("%d-%s", i, strings.Join(tc.path, "/")), mkTest(tc))
110+
}
111+
}

http/http_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"reflect"
1010
"runtime"
11+
"strings"
1112
"testing"
1213

1314
"github.com/ipfs/go-ipfs-cmds"
@@ -71,6 +72,7 @@ func TestHTTP(t *testing.T) {
7172
}
7273

7374
v, err := res.Next()
75+
t.Log("v:", v, "err:", err)
7476
if tc.err != nil {
7577
if err == nil {
7678
t.Error("got nil error, expected:", tc.err)
@@ -132,6 +134,6 @@ func TestHTTP(t *testing.T) {
132134
}
133135

134136
for i, tc := range tcs {
135-
t.Run(fmt.Sprint(i), mkTest(tc))
137+
t.Run(fmt.Sprintf("%d-%s", i, strings.Join(tc.path, "/")), mkTest(tc))
136138
}
137139
}

http/parse.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,10 @@ func parseResponse(httpRes *http.Response, req *cmds.Request) (cmds.Response, er
218218
return nil, fmt.Errorf("unknown error content type: %s", contentType)
219219
default:
220220
// handle errors from headers
221-
e.Message = httpRes.Header.Get(StreamErrHeader)
221+
err := res.dec.Decode(e)
222+
if err != nil {
223+
log.Errorf("error parsing error: ", err.Error())
224+
}
222225
}
223226

224227
res.initErr = e

http/responseemitter.go

+35-27
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ type responseEmitter struct {
7070
}
7171

7272
func (re *responseEmitter) Emit(value interface{}) error {
73-
7473
// Initially this library allowed commands to return errors by sending an
7574
// error value along a stream. We removed that in favour of CloseWithError,
7675
// so we want to make sure we catch situations where some code still uses the
@@ -112,19 +111,23 @@ func (re *responseEmitter) Emit(value interface{}) error {
112111
isSingle = true
113112
}
114113

114+
if f, ok := re.w.(http.Flusher); ok {
115+
defer f.Flush()
116+
}
117+
115118
switch v := value.(type) {
119+
case error:
120+
return re.closeWithError(v)
116121
case io.Reader:
117122
err = flushCopy(re.w, v)
118123
default:
119124
err = re.enc.Encode(value)
120125
}
121126

122-
if f, ok := re.w.(http.Flusher); ok {
123-
f.Flush()
124-
}
125-
126127
if isSingle {
127-
err = re.closeWithError(err)
128+
// always close singles with nil error.
129+
// encoding errors go to caller.
130+
err = re.closeWithError(nil)
128131
}
129132

130133
return err
@@ -148,16 +151,13 @@ func (re *responseEmitter) CloseWithError(err error) error {
148151
re.l.Lock()
149152
defer re.l.Unlock()
150153

151-
if re.closed {
152-
return cmds.ErrClosingClosedEmitter
153-
}
154-
155154
return re.closeWithError(err)
156155
}
157156

158157
func (re *responseEmitter) closeWithError(err error) error {
159-
// encoding error, only set if err != nil/EOF
160-
var encErr error
158+
if re.closed {
159+
return cmds.ErrClosingClosedEmitter
160+
}
161161

162162
if err == io.EOF {
163163
err = nil
@@ -166,27 +166,24 @@ func (re *responseEmitter) closeWithError(err error) error {
166166
err = &e
167167
}
168168

169+
setErrTrailer := true
170+
169171
// use preamble directly, we're already in critical section
170172
// preamble needs to be before branch below, because the headers need to be written before writing the response
171-
re.once.Do(func() { re.doPreamble(err) })
173+
re.once.Do(func() {
174+
re.doPreamble(err)
172175

173-
if err != nil {
174-
re.w.Header().Set(StreamErrHeader, err.Error())
176+
// do not set error trailer if we send the error as value in preamble
177+
setErrTrailer = false
178+
})
175179

176-
// also send the error as a value if we have an encoder
177-
if re.enc != nil {
178-
e, ok := err.(*cmdkit.Error)
179-
if !ok {
180-
e = &cmdkit.Error{Message: err.Error()}
181-
}
182-
183-
encErr = re.enc.Encode(e)
184-
}
180+
if setErrTrailer && err != nil {
181+
re.w.Header().Set(StreamErrHeader, err.Error())
185182
}
186183

187184
re.closed = true
188185

189-
return encErr
186+
return nil
190187
}
191188

192189
// Flush the http connection
@@ -229,10 +226,8 @@ func (re *responseEmitter) doPreamble(value interface{}) {
229226
} else {
230227
status = http.StatusInternalServerError
231228
}
232-
h.Set(StreamErrHeader, err.Message)
233229
case error:
234230
status = http.StatusInternalServerError
235-
h.Set(StreamErrHeader, v.Error())
236231
default:
237232
h.Set(channelHeader, "1")
238233
}
@@ -259,6 +254,19 @@ func (re *responseEmitter) doPreamble(value interface{}) {
259254
h.Set("Access-Control-Expose-Headers", AllowedExposedHeaders)
260255

261256
re.w.WriteHeader(status)
257+
258+
if err, ok := value.(error); ok {
259+
if _, ok := err.(*cmdkit.Error); !ok {
260+
err = &cmdkit.Error{Message: err.Error()}
261+
}
262+
263+
err = re.enc.Encode(err)
264+
if err != nil {
265+
log.Error("error sending error value after non-200 response", err)
266+
}
267+
268+
re.closed = true
269+
}
262270
}
263271

264272
type responseWriterer interface {

0 commit comments

Comments
 (0)