diff --git a/api/batch.go b/api/batch.go index b6232dd..7fc6c4a 100644 --- a/api/batch.go +++ b/api/batch.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "sync" "time" @@ -79,6 +78,19 @@ func Batch[T any](c *Client, tasks []BatchTask[T], opts ...BatchOption) ([]mo.Re return results, nil } +// errorRaw builds a JSON object {"error": ""} with the message properly +// escaped. Interpolating err.Error() directly produced invalid JSON whenever the +// error string contained quotes (e.g. *url.Error: `Get "http://host": ...`), +// breaking the marshal of the whole batch. +func errorRaw(msg string) *json.RawMessage { + b, err := json.Marshal(map[string]string{"error": msg}) + if err != nil { + b = []byte(`{"error": "failed to marshal error message"}`) + } + raw := json.RawMessage(b) + return &raw +} + func BatchResultToRaw(r mo.Result[*Response]) *json.RawMessage { err := r.Error() if err != nil { @@ -86,14 +98,12 @@ func BatchResultToRaw(r mo.Result[*Response]) *json.RawMessage { if ok { return &jsonErr.Raw } - errRaw := json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())) - return &errRaw + return errorRaw(err.Error()) } resp := r.MustGet() raw, err := resp.ToJSON() if err != nil { - errRaw := json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())) - return &errRaw + return errorRaw(err.Error()) } return raw } diff --git a/api/batch_test.go b/api/batch_test.go index 2bbfd1d..6ed1e04 100644 --- a/api/batch_test.go +++ b/api/batch_test.go @@ -2,6 +2,8 @@ package api import ( "context" + "encoding/json" + "errors" "net/http" "testing" @@ -40,3 +42,16 @@ func TestBatch(t *testing.T) { assert.Equal(t, results[0].MustGet().StatusCode, http.StatusOK) assert.Equal(t, results[1].MustGet().StatusCode, http.StatusOK) } + +func TestBatchResultToRawEscapesQuotesInError(t *testing.T) { + // *url.Error-style messages contain quotes; they must not break the JSON. + err := errors.New(`Get "http://example.com/": context deadline exceeded`) + raw := BatchResultToRaw(mo.Err[*Response](err)) + + _, jsonErr := json.Marshal(raw) + assert.NoError(t, jsonErr) + + var obj map[string]string + assert.NoError(t, json.Unmarshal(*raw, &obj)) + assert.Equal(t, err.Error(), obj["error"]) +}