Skip to content

Commit acf6f46

Browse files
Merge pull request #15 from antoineco/typed-errors
Return typed HEC error responses
2 parents c9590e2 + 659cc01 commit acf6f46

File tree

3 files changed

+210
-7
lines changed

3 files changed

+210
-7
lines changed

splunk/response.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package splunk
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// EventCollectorResponse is the payload returned by the HTTP Event Collector
11+
// in response to requests.
12+
// https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector
13+
type EventCollectorResponse struct {
14+
Text string `json:"text"`
15+
Code StatusCode `json:"code"`
16+
InvalidEventNumber *int `json:"invalid-event-number"`
17+
AckID *int `json:"ackId"`
18+
}
19+
20+
var _ error = (*EventCollectorResponse)(nil)
21+
22+
// Error implements the error interface.
23+
func (r *EventCollectorResponse) Error() string {
24+
if r == nil {
25+
return ""
26+
}
27+
28+
var sb strings.Builder
29+
30+
sb.WriteString(r.Text + " (Code: " + strconv.Itoa(int(r.Code)))
31+
if r.InvalidEventNumber != nil {
32+
sb.WriteString(", InvalidEventNumber: " + strconv.Itoa(*r.InvalidEventNumber))
33+
}
34+
if r.AckID != nil {
35+
sb.WriteString(", AckID: " + strconv.Itoa(*r.AckID))
36+
}
37+
sb.WriteRune(')')
38+
39+
return sb.String()
40+
}
41+
42+
// StatusCode defines the meaning of responses returned by HTTP Event Collector
43+
// endpoints.
44+
type StatusCode int8
45+
46+
const (
47+
Success StatusCode = iota
48+
TokenDisabled
49+
TokenRequired
50+
InvalidAuthz
51+
InvalidToken
52+
NoData
53+
InvalidDataFormat
54+
IncorrectIndex
55+
InternalServerError
56+
ServerBusy
57+
DataChannelMissing
58+
InvalidDataChannel
59+
EventFieldRequired
60+
EventFieldBlank
61+
ACKDisabled
62+
ErrorHandlingIndexedFields
63+
QueryStringAuthzNotEnabled
64+
)
65+
66+
// HTTPCode returns the HTTP code corresponding to the given StatusCode. It
67+
// returns -1 and an error in case the HTTP status code can not be determined.
68+
func (c StatusCode) HTTPCode() (code int, err error) {
69+
switch c {
70+
case Success:
71+
code = http.StatusOK
72+
case TokenDisabled:
73+
code = http.StatusForbidden
74+
case TokenRequired:
75+
code = http.StatusUnauthorized
76+
case InvalidAuthz:
77+
code = http.StatusUnauthorized
78+
case InvalidToken:
79+
code = http.StatusForbidden
80+
case NoData:
81+
code = http.StatusBadRequest
82+
case InvalidDataFormat:
83+
code = http.StatusBadRequest
84+
case IncorrectIndex:
85+
code = http.StatusBadRequest
86+
case InternalServerError:
87+
code = http.StatusInternalServerError
88+
case ServerBusy:
89+
code = http.StatusServiceUnavailable
90+
case DataChannelMissing:
91+
code = http.StatusBadRequest
92+
case InvalidDataChannel:
93+
code = http.StatusBadRequest
94+
case EventFieldRequired:
95+
code = http.StatusBadRequest
96+
case EventFieldBlank:
97+
code = http.StatusBadRequest
98+
case ACKDisabled:
99+
code = http.StatusBadRequest
100+
case ErrorHandlingIndexedFields:
101+
code = http.StatusBadRequest
102+
case QueryStringAuthzNotEnabled:
103+
code = http.StatusBadRequest
104+
default:
105+
code = -1
106+
err = fmt.Errorf("unknown status code %d", c)
107+
}
108+
109+
return
110+
}

splunk/response_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package splunk
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
)
7+
8+
func TestEventCollectorResponseError(t *testing.T) {
9+
invalidEventNumber := 2
10+
ackID := 12345
11+
12+
testCases := []struct {
13+
name string
14+
input *EventCollectorResponse
15+
expect string
16+
}{
17+
{
18+
name: "Response is nil",
19+
input: nil,
20+
expect: "",
21+
}, {
22+
name: "All response attributes are set",
23+
input: &EventCollectorResponse{
24+
Text: "An error",
25+
Code: 10,
26+
InvalidEventNumber: &invalidEventNumber,
27+
AckID: &ackID,
28+
},
29+
expect: "An error (Code: 10, InvalidEventNumber: 2, AckID: 12345)",
30+
}, {
31+
name: "Some response attributes are set",
32+
input: &EventCollectorResponse{
33+
Text: "An error",
34+
Code: 10,
35+
AckID: &ackID,
36+
},
37+
expect: "An error (Code: 10, AckID: 12345)",
38+
},
39+
}
40+
41+
for _, tc := range testCases {
42+
t.Run(tc.name, func(t *testing.T) {
43+
errStr := tc.input.Error()
44+
if errStr != tc.expect {
45+
t.Errorf("Expected %q, got %q", tc.expect, errStr)
46+
}
47+
})
48+
}
49+
}
50+
51+
func TestStatusCodeHTTPCode(t *testing.T) {
52+
testCases := []struct {
53+
name string
54+
input StatusCode
55+
expectCode int
56+
expectErr bool
57+
}{
58+
{
59+
name: "Known status code",
60+
input: IncorrectIndex,
61+
expectCode: http.StatusBadRequest,
62+
expectErr: false,
63+
}, {
64+
name: "Unknown status code",
65+
input: StatusCode(100),
66+
expectCode: -1,
67+
expectErr: true,
68+
},
69+
}
70+
71+
for _, tc := range testCases {
72+
t.Run(tc.name, func(t *testing.T) {
73+
code, err := tc.input.HTTPCode()
74+
75+
if !tc.expectErr && err != nil {
76+
t.Fatalf("Unexpected error: %s", err)
77+
}
78+
if tc.expectErr && err == nil {
79+
t.Fatalf("Expected an error to occur")
80+
}
81+
82+
if code != tc.expectCode {
83+
t.Errorf("Expected %d, got %d", tc.expectCode, code)
84+
}
85+
})
86+
}
87+
}

splunk/splunk.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,19 +161,25 @@ func (c *Client) doRequest(b *bytes.Buffer) error {
161161
// need to make sure we close the body to avoid hanging the connection
162162
defer res.Body.Close()
163163

164-
// If statusCode is not good, return error string
164+
// If statusCode is not OK, return the error
165165
switch res.StatusCode {
166166
case 200:
167167
// need to read the reply otherwise the connection hangs
168168
io.Copy(ioutil.Discard, res.Body)
169169
return nil
170170
default:
171-
// Turn response into string and return it
172-
buf := new(bytes.Buffer)
173-
buf.ReadFrom(res.Body)
174-
responseBody := buf.String()
175-
err = errors.New(responseBody)
171+
respBody, err := ioutil.ReadAll(res.Body)
172+
if err != nil {
173+
return err
174+
}
175+
176+
// try deserializing response body to a typed HEC response
177+
hecResp := &EventCollectorResponse{}
178+
if err := json.Unmarshal(respBody, hecResp); err == nil {
179+
return hecResp
180+
}
176181

182+
// otherwise, return the response body as an error string
183+
return errors.New(string(respBody))
177184
}
178-
return err
179185
}

0 commit comments

Comments
 (0)