Replace io.ReadAll + json.Unmarshal with streaming JSON decoder

Refactor the internal HTTP helper from get() returning raw bytes to
getAndDecode() that streams directly into the target struct via
json.NewDecoder. This eliminates the intermediate []byte allocation
from io.ReadAll on every API response.

The new decoder also validates that responses contain exactly one JSON
value by attempting a second Decode after the primary one — any content
beyond the first value (e.g., concatenated objects from a misbehaving
proxy) returns an error instead of silently discarding it.

Changes:
- api/client.go: Replace get() with getAndDecode(), update FetchStores
  and FetchSavings callers to use the new signature
- api/client_test.go: Add TestFetchSavings_TrailingJSONIsRejected and
  TestFetchStores_MalformedJSONReturnsDecodeError covering the new
  decoder error paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 00:11:24 -05:00
parent 4f483c82e5
commit 4310375dc9
2 changed files with 49 additions and 27 deletions

View File

@@ -127,6 +127,34 @@ func TestFetchStores_NoResults(t *testing.T) {
assert.Empty(t, result)
}
func TestFetchSavings_TrailingJSONIsRejected(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"Savings":[],"LanguageId":1} {"extra":true}`))
}))
defer srv.Close()
client := api.NewClientWithBaseURLs(srv.URL, "")
_, err := client.FetchSavings(context.Background(), "1425")
assert.Error(t, err)
assert.Contains(t, err.Error(), "decoding")
}
func TestFetchStores_MalformedJSONReturnsDecodeError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"Stores":`))
}))
defer srv.Close()
client := api.NewClientWithBaseURLs("", srv.URL)
_, err := client.FetchStores(context.Background(), "37042", 5)
assert.Error(t, err)
assert.Contains(t, err.Error(), "decoding")
}
func TestStoreNumber(t *testing.T) {
tests := []struct {
input string