Skip to content

Commit cff46ca

Browse files
fix: return InfluxQL JSON marshaling errors correctly (#27318)
JSON cannot marshal NaN and Inf, so results which contain them should return marshaling errors Closes #20258
1 parent b6bc458 commit cff46ca

2 files changed

Lines changed: 151 additions & 9 deletions

File tree

influxql/query/response_writer.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,49 @@ func (f *jsonFormatter) WriteResponse(ctx context.Context, w io.Writer, resp Res
4646
span, _ := tracing.StartSpanFromContext(ctx)
4747
defer span.Finish()
4848

49+
var wErr error
4950
var b []byte
50-
if f.Pretty {
51-
b, err = json.MarshalIndent(resp, "", " ")
52-
} else {
53-
b, err = json.Marshal(resp)
54-
}
51+
b, err = f.marshal(resp)
5552

5653
if err != nil {
57-
_, err = io.WriteString(w, err.Error())
54+
nakedErr := unnestError(err)
55+
errResult := &Result{Err: nakedErr}
56+
if len(resp.Results) >= 1 && resp.Results[0] != nil {
57+
errResult.StatementID = resp.Results[0].StatementID
58+
}
59+
// We're so deep in errors we are going to drop this one.
60+
b, _ = f.marshal(Response{Results: []*Result{errResult}})
61+
}
62+
if _, wErr = w.Write(b); wErr == nil {
63+
_, wErr = w.Write([]byte("\n"))
64+
}
65+
return wErr
66+
}
67+
68+
func (f *jsonFormatter) marshal(resp Response) ([]byte, error) {
69+
if f.Pretty {
70+
return json.MarshalIndent(resp, "", " ")
5871
} else {
59-
_, err = w.Write(b)
72+
return json.Marshal(resp)
6073
}
74+
}
6175

62-
w.Write([]byte("\n"))
63-
return err
76+
func unnestError(err error) error {
77+
for err != nil {
78+
switch e := err.(type) {
79+
case interface{ Unwrap() error }:
80+
err = e.Unwrap()
81+
case interface{ Unwrap() []error }:
82+
errs := e.Unwrap()
83+
if len(errs) == 0 {
84+
return err
85+
}
86+
err = errs[0]
87+
default:
88+
return err
89+
}
90+
}
91+
return nil
6492
}
6593

6694
type csvFormatter struct {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package query
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"math"
8+
"testing"
9+
10+
"github.com/influxdata/influxdb/v2/models"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestJsonFormatter_WriteResponse_Success(t *testing.T) {
16+
resp := Response{
17+
Results: []*Result{{
18+
StatementID: 0,
19+
Series: models.Rows{
20+
&models.Row{
21+
Name: "cpu",
22+
Columns: []string{"time", "value"},
23+
Values: [][]interface{}{{"2021-01-01T00:00:00Z", 42.0}},
24+
},
25+
},
26+
}},
27+
}
28+
29+
for _, pretty := range []bool{false, true} {
30+
name := "compact"
31+
if pretty {
32+
name = "pretty"
33+
}
34+
t.Run(name, func(t *testing.T) {
35+
var buf bytes.Buffer
36+
f := &jsonFormatter{Pretty: pretty}
37+
err := f.WriteResponse(context.Background(), &buf, resp)
38+
require.NoError(t, err)
39+
40+
output := buf.Bytes()
41+
var parsed map[string]interface{}
42+
require.NoError(t, json.Unmarshal(output, &parsed))
43+
44+
results, ok := parsed["results"].([]interface{})
45+
require.True(t, ok, "expected results array, got: %s", output)
46+
require.NotEmpty(t, results)
47+
48+
result0, ok := results[0].(map[string]interface{})
49+
require.True(t, ok, "expected result object, got: %s", output)
50+
assert.Nil(t, result0["error"], "unexpected error in result")
51+
52+
series, ok := result0["series"].([]interface{})
53+
require.True(t, ok, "expected series array, got: %s", output)
54+
require.NotEmpty(t, series)
55+
56+
if pretty {
57+
assert.Contains(t, string(output), " ", "expected indented output for pretty mode")
58+
}
59+
})
60+
}
61+
}
62+
63+
func TestJsonFormatter_WriteResponse_MarshalError(t *testing.T) {
64+
const statementID = 12
65+
tests := []struct {
66+
name string
67+
value interface{}
68+
wantErr string
69+
}{
70+
{"Inf", math.Inf(1), "unsupported value"},
71+
{"NaN", math.NaN(), "unsupported value"},
72+
}
73+
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
resp := Response{
77+
Results: []*Result{{
78+
StatementID: statementID,
79+
Series: models.Rows{
80+
&models.Row{
81+
Name: "cpu",
82+
Columns: []string{"time", "value"},
83+
Values: [][]interface{}{{"2021-01-01T00:00:00Z", tt.value}},
84+
},
85+
},
86+
}},
87+
}
88+
89+
var buf bytes.Buffer
90+
f := &jsonFormatter{Pretty: false}
91+
err := f.WriteResponse(context.Background(), &buf, resp)
92+
require.NoError(t, err)
93+
94+
output := buf.Bytes()
95+
var parsed map[string]interface{}
96+
require.NoError(t, json.Unmarshal(output, &parsed), "output is not valid JSON: %s", output)
97+
98+
results, ok := parsed["results"].([]interface{})
99+
require.True(t, ok, "expected results array, got: %s", output)
100+
require.NotEmpty(t, results)
101+
102+
result0, ok := results[0].(map[string]interface{})
103+
require.True(t, ok, "expected result object, got: %s", output)
104+
105+
errMsg, ok := result0["error"].(string)
106+
require.True(t, ok, "expected error string in result, got: %s", output)
107+
require.Contains(t, errMsg, tt.wantErr)
108+
109+
sid, ok := result0["statement_id"].(float64)
110+
require.True(t, ok, "expected statement_id in error result, got: %s", output)
111+
assert.Equal(t, float64(statementID), sid, "statement_id should match original")
112+
})
113+
}
114+
}

0 commit comments

Comments
 (0)