Skip to content

Commit bc2cbf4

Browse files
authored
Dump artifact uri response body when fail decoding (#40563)
* Dump http response when decoding error occurs * Add status code check on artifact response for snapshot downloader * Add checks on content type of artifact api response * use httputils to dump the full response
1 parent ebd70bf commit bc2cbf4

File tree

2 files changed

+198
-4
lines changed

2 files changed

+198
-4
lines changed

x-pack/elastic-agent/pkg/artifact/download/snapshot/downloader.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ package snapshot
77
import (
88
"context"
99
"encoding/json"
10+
goerrors "errors"
1011
"fmt"
12+
"mime"
13+
gohttp "net/http"
14+
"net/http/httputil"
1115
"strings"
1216

1317
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors"
@@ -97,19 +101,28 @@ func snapshotURI(versionOverride string, config *artifact.Config) (string, error
97101
}
98102

99103
artifactsURI := fmt.Sprintf("https://artifacts-api.elastic.co/v1/search/%s-SNAPSHOT/elastic-agent", version)
100-
resp, err := client.Get(artifactsURI)
104+
req, err := gohttp.NewRequestWithContext(context.Background(), gohttp.MethodGet, artifactsURI, nil)
105+
if err != nil {
106+
return "", fmt.Errorf("creating artifacts API request to %q: %w", artifactsURI, err)
107+
}
108+
109+
resp, err := client.Do(req)
101110
if err != nil {
102111
return "", err
103112
}
104113
defer resp.Body.Close()
105114

115+
err = checkResponse(resp)
116+
if err != nil {
117+
return "", fmt.Errorf("checking artifacts api response: %w", err)
118+
}
119+
106120
body := struct {
107121
Packages map[string]interface{} `json:"packages"`
108122
}{}
109123

110-
dec := json.NewDecoder(resp.Body)
111-
if err := dec.Decode(&body); err != nil {
112-
return "", err
124+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
125+
return "", fmt.Errorf("decoding GET %s response: %w", artifactsURI, err)
113126
}
114127

115128
if len(body.Packages) == 0 {
@@ -149,3 +162,33 @@ func snapshotURI(versionOverride string, config *artifact.Config) (string, error
149162

150163
return "", fmt.Errorf("uri not detected")
151164
}
165+
166+
func checkResponse(resp *gohttp.Response) error {
167+
if resp.StatusCode != gohttp.StatusOK {
168+
responseDump, dumpErr := httputil.DumpResponse(resp, true)
169+
if dumpErr != nil {
170+
return goerrors.Join(fmt.Errorf("unsuccessful status code %d in artifactsURI response", resp.StatusCode), fmt.Errorf("dumping response: %w", dumpErr))
171+
}
172+
return fmt.Errorf("unsuccessful status code %d in artifactsURI\nfull response:\n%s", resp.StatusCode, responseDump)
173+
}
174+
175+
responseContentType := resp.Header.Get("Content-Type")
176+
mediatype, _, err := mime.ParseMediaType(responseContentType)
177+
if err != nil {
178+
responseDump, dumpErr := httputil.DumpResponse(resp, true)
179+
if dumpErr != nil {
180+
return goerrors.Join(fmt.Errorf("parsing content-type %q: %w", responseContentType, err), fmt.Errorf("dumping response: %w", dumpErr))
181+
}
182+
return fmt.Errorf("parsing content-type %q: %w\nfull response:\n%s", responseContentType, err, responseDump)
183+
}
184+
185+
if mediatype != "application/json" {
186+
responseDump, dumpErr := httputil.DumpResponse(resp, true)
187+
if dumpErr != nil {
188+
return goerrors.Join(fmt.Errorf("unexpected media type in artifacts API response %q (parsed from %q)", mediatype, responseContentType), fmt.Errorf("dumping response: %w", dumpErr))
189+
}
190+
return fmt.Errorf("unexpected media type in artifacts API response %q (parsed from %q), response:\n%s", mediatype, responseContentType, responseDump)
191+
}
192+
193+
return nil
194+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package snapshot
6+
7+
import (
8+
"bytes"
9+
"io"
10+
gohttp "net/http"
11+
"strconv"
12+
"strings"
13+
"testing"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func Test_checkResponse(t *testing.T) {
19+
type args struct {
20+
resp *gohttp.Response
21+
}
22+
tests := []struct {
23+
name string
24+
args args
25+
wantErr assert.ErrorAssertionFunc
26+
}{
27+
{
28+
name: "Valid http response",
29+
args: args{
30+
resp: &gohttp.Response{
31+
Status: "OK",
32+
StatusCode: gohttp.StatusOK,
33+
Header: map[string][]string{
34+
"Content-Type": {"application/json; charset=UTF-8"},
35+
},
36+
Body: io.NopCloser(strings.NewReader(`{"good": "job"}`)),
37+
},
38+
},
39+
wantErr: assert.NoError,
40+
},
41+
{
42+
name: "Bad http status code - 500",
43+
args: args{
44+
resp: &gohttp.Response{
45+
Status: "Not OK",
46+
StatusCode: gohttp.StatusInternalServerError,
47+
Header: map[string][]string{
48+
"Content-Type": {"application/json"},
49+
},
50+
Body: io.NopCloser(strings.NewReader(`{"not feeling": "too well"}`)),
51+
},
52+
},
53+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
54+
return assert.ErrorContains(t, err, strconv.Itoa(gohttp.StatusInternalServerError), "error should contain http status code") &&
55+
assert.ErrorContains(t, err, `{"not feeling": "too well"}`, "error should contain response body")
56+
},
57+
},
58+
{
59+
name: "Bad http status code - 502",
60+
args: args{
61+
resp: &gohttp.Response{
62+
Status: "Bad Gateway",
63+
StatusCode: gohttp.StatusBadGateway,
64+
Header: map[string][]string{
65+
"Content-Type": {"application/json; charset=UTF-8"},
66+
},
67+
Body: io.NopCloser(strings.NewReader(`{"bad": "gateway"}`)),
68+
},
69+
},
70+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
71+
return assert.ErrorContains(t, err, strconv.Itoa(gohttp.StatusBadGateway), "error should contain http status code") &&
72+
assert.ErrorContains(t, err, `{"bad": "gateway"}`, "error should contain response body")
73+
},
74+
},
75+
{
76+
name: "Bad http status code - 503",
77+
args: args{
78+
resp: &gohttp.Response{
79+
Status: "Service Unavailable",
80+
StatusCode: gohttp.StatusServiceUnavailable,
81+
Header: map[string][]string{
82+
"Content-Type": {"application/json"},
83+
},
84+
Body: io.NopCloser(bytes.NewReader([]byte{})),
85+
},
86+
},
87+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
88+
return assert.ErrorContains(t, err, strconv.Itoa(gohttp.StatusServiceUnavailable), "error should contain http status code")
89+
},
90+
},
91+
{
92+
name: "Bad http status code - 504",
93+
args: args{
94+
resp: &gohttp.Response{
95+
Status: "Gateway timed out",
96+
StatusCode: gohttp.StatusGatewayTimeout,
97+
Header: map[string][]string{
98+
"Content-Type": {"application/json; charset=UTF-8"},
99+
},
100+
Body: io.NopCloser(strings.NewReader(`{"gateway": "never got back to me"}`)),
101+
},
102+
},
103+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
104+
return assert.ErrorContains(t, err, strconv.Itoa(gohttp.StatusGatewayTimeout), "error should contain http status code") &&
105+
assert.ErrorContains(t, err, `{"gateway": "never got back to me"}`, "error should contain response body")
106+
},
107+
},
108+
{
109+
name: "Wrong content type: XML",
110+
args: args{
111+
resp: &gohttp.Response{
112+
Status: "XML is back in, baby",
113+
StatusCode: gohttp.StatusOK,
114+
Header: map[string][]string{
115+
"Content-Type": {"application/xml"},
116+
},
117+
Body: io.NopCloser(strings.NewReader(`<?xml version='1.0' encoding='UTF-8'?><note>Those who cannot remember the past are condemned to repeat it.</note>`)),
118+
},
119+
},
120+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
121+
return assert.ErrorContains(t, err, "application/xml") &&
122+
assert.ErrorContains(t, err, `<?xml version='1.0' encoding='UTF-8'?><note>Those who cannot remember the past are condemned to repeat it.</note>`)
123+
},
124+
},
125+
{
126+
name: "Wrong content type: HTML",
127+
args: args{
128+
resp: &gohttp.Response{
129+
Status: "HTML is always (not) machine-friendly",
130+
StatusCode: gohttp.StatusOK,
131+
Header: map[string][]string{
132+
"Content-Type": {"text/html"},
133+
},
134+
Body: io.NopCloser(strings.NewReader(`<!DOCTYPE html><html><body>Hello world!</body></html>`)),
135+
},
136+
},
137+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
138+
return assert.ErrorContains(t, err, "text/html") &&
139+
assert.ErrorContains(t, err, `<!DOCTYPE html><html><body>Hello world!</body></html>`)
140+
},
141+
},
142+
}
143+
for _, tt := range tests {
144+
t.Run(tt.name, func(t *testing.T) {
145+
err := checkResponse(tt.args.resp)
146+
if !tt.wantErr(t, err) {
147+
return
148+
}
149+
})
150+
}
151+
}

0 commit comments

Comments
 (0)