Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x-pack/filebeat/input/httpjson: make response body decoding errors more informative #36481

Merged
merged 2 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]
- [Azure] Add input metrics to the azure-eventhub input. {pull}35739[35739]
- Reduce HTTPJSON metrics allocations. {pull}36282[36282]
- Add support for a simplified input configuraton when running under Elastic-Agent {pull}36390[36390]
- Make HTTPJSON response body decoding errors more informative. {pull}36481[36481]

*Auditbeat*

Expand Down
112 changes: 107 additions & 5 deletions x-pack/filebeat/input/httpjson/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
"bytes"
"encoding/csv"
"encoding/json"
stdxml "encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"unicode"

"github.com/elastic/mito/lib/xml"
)
Expand Down Expand Up @@ -72,7 +75,11 @@

// decodeAsJSON decodes the JSON message in p into dst.
func decodeAsJSON(p []byte, dst *response) error {
return json.Unmarshal(p, &dst.body)
err := json.Unmarshal(p, &dst.body)
if err != nil {
return textContextError{error: err, body: p}
}
return nil
}

// encodeAsForm encodes trReq as a URL encoded form.
Expand All @@ -90,12 +97,12 @@
// decodeAsNdjson decodes the message in p as a JSON object stream
// It is more relaxed than NDJSON.
func decodeAsNdjson(p []byte, dst *response) error {
var results []interface{}

Check failure on line 100 in x-pack/filebeat/input/httpjson/encoding.go

View workflow job for this annotation

GitHub Actions / lint (windows)

Consider pre-allocating `results` (prealloc)
dec := json.NewDecoder(bytes.NewReader(p))
for dec.More() {
var o interface{}
if err := dec.Decode(&o); err != nil {
return err
return textContextError{error: err, body: p}
}
results = append(results, o)
}
Expand All @@ -105,7 +112,7 @@

// decodeAsCSV decodes p as a headed CSV document into dst.
func decodeAsCSV(p []byte, dst *response) error {
var results []interface{}

Check failure on line 115 in x-pack/filebeat/input/httpjson/encoding.go

View workflow job for this annotation

GitHub Actions / lint (windows)

Consider pre-allocating `results` (prealloc)

r := csv.NewReader(bytes.NewReader(p))

Expand Down Expand Up @@ -135,7 +142,7 @@

if err != nil {
if err != io.EOF { //nolint:errorlint // csv.Reader never wraps io.EOF.
return err
return textContextError{error: err, body: p}
}
}

Expand Down Expand Up @@ -165,7 +172,7 @@
var o interface{}
if err := dec.Decode(&o); err != nil {
rc.Close()
return err
return textContextError{error: err, body: p}
}
results = append(results, o)
}
Expand All @@ -185,9 +192,104 @@
func decodeAsXML(p []byte, dst *response) error {
cdata, body, err := xml.Unmarshal(bytes.NewReader(p), dst.xmlDetails)
if err != nil {
return err
return textContextError{error: err, body: p}
}
dst.body = body
dst.header["XML-CDATA"] = []string{cdata}
return nil
}

// textContextError is an error that can provide the text context for
// a decoding error from the csv, json and xml packages.
type textContextError struct {
error
body []byte
}

func (e textContextError) Error() string {
var ctx []byte
switch err := e.error.(type) {

Check failure on line 211 in x-pack/filebeat/input/httpjson/encoding.go

View workflow job for this annotation

GitHub Actions / lint (windows)

type switch on error will fail on wrapped errors. Use errors.As to check for specific errors (errorlint)
case nil:
return "<nil>"
case *json.SyntaxError:
ctx = textContext(e.body, err.Offset)
case *json.UnmarshalTypeError:
ctx = textContext(e.body, err.Offset)
case *csv.ParseError:
lines := bytes.Split(e.body, []byte{'\n'})
l := err.Line - 1 // Lines are 1-based.
if uint(l) >= uint(len(lines)) {
return err.Error()
}
ctx = textContext(lines[l], int64(err.Column))
case *stdxml.SyntaxError:
lines := bytes.Split(e.body, []byte{'\n'})
l := err.Line - 1 // Lines are 1-based.
if uint(l) >= uint(len(lines)) {
return err.Error()
}
// The xml package does not provide column-level context,
// so just point to first non-whitespace character of the
// line. This doesn't make a great deal of difference
// except in deeply indented XML documents.
pos := bytes.IndexFunc(lines[l], func(r rune) bool {
return !unicode.IsSpace(r)
})
if pos < 0 {
pos = 0
}
ctx = textContext(lines[l], int64(pos))
default:
return err.Error()
}
return fmt.Sprintf("%v: text context %q", e.error, ctx)
}

func (e textContextError) Unwrap() error {
return e.error
}

// textContext returns the context of text around the provided position starting
// ten bytes before pos and ten bytes after, dependent on the length of the
// text and the value of pos relative to bounds. If a text truncation is made,
// an ellipsis is added to indicate this.
func textContext(text []byte, pos int64) []byte {
if len(text) == 0 {
return text
}
const (
dots = "..."
span = 10
)
left := maxInt64(0, pos-span)
right := minInt(pos+span+1, int64(len(text)))
ctx := make([]byte, right-left+2*int64(len(dots)))
copy(ctx[3:], text[left:right])
if left != 0 {
copy(ctx, dots)
left = 0
} else {
left = int64(len(dots))
}
if right != int64(len(text)) {
copy(ctx[len(ctx)-len(dots):], dots)
right = int64(len(ctx))
} else {
right = int64(len(ctx) - len(dots))
}
return ctx[left:right]
}

func minInt(a, b int64) int64 {
if a < b {
return a
}
return b
}

func maxInt64(a, b int64) int64 {
if a > b {
return a
}
return b
}