From 7ffc6e81cea7408049c8ff0956c49a7d9ceec8ff Mon Sep 17 00:00:00 2001 From: Quadrubo <71718414+Quadrubo@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:40:24 +0200 Subject: [PATCH] fix(agent): prevent a malformed Docker response from permanently breaking container stats (#2066) decode() reused a single json.Decoder across responses. Once a decode errored on a truncated or malformed body (e.g. a container removed mid-request), the decoder stayed desynced and every subsequent decode failed, silently stopping all container stats until the agent was restarted. Decode the buffered bytes with json.Unmarshal instead, which keeps the reusable read buffer but holds no cross-call state. Regression from d67d638. --- agent/docker.go | 9 +++------ agent/docker_test.go | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/agent/docker.go b/agent/docker.go index f35f7fc8..3ac3fd6d 100644 --- a/agent/docker.go +++ b/agent/docker.go @@ -65,7 +65,6 @@ type dockerManager struct { dockerVersionChecked bool // Whether a version probe has completed successfully isWindows bool // Whether the Docker Engine API is running on Windows buf *bytes.Buffer // Buffer to store and read response bodies - decoder *json.Decoder // Reusable JSON decoder that reads from buf apiStats *container.ApiStats // Reusable API stats object excludeContainers []string // Patterns to exclude containers by name usingPodman bool // Whether the Docker Engine API is running on Podman @@ -747,20 +746,18 @@ func (dm *dockerManager) applyDockerVersionInfo(serverHeader string, versionInfo } } -// Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe. +// Decodes a Docker API JSON response using a reusable buffer. Not thread safe. func (dm *dockerManager) decode(resp *http.Response, d any) error { if dm.buf == nil { // initialize buffer with 256kb starting size dm.buf = bytes.NewBuffer(make([]byte, 0, 1024*256)) - dm.decoder = json.NewDecoder(dm.buf) } defer resp.Body.Close() defer dm.buf.Reset() - _, err := dm.buf.ReadFrom(resp.Body) - if err != nil { + if _, err := dm.buf.ReadFrom(resp.Body); err != nil { return err } - return dm.decoder.Decode(d) + return json.Unmarshal(dm.buf.Bytes(), d) } // Test docker / podman sockets and return if one exists diff --git a/agent/docker_test.go b/agent/docker_test.go index 9186a756..d4485084 100644 --- a/agent/docker_test.go +++ b/agent/docker_test.go @@ -804,6 +804,24 @@ func TestGetDockerStatsRetriesVersionCheckUntilSuccess(t *testing.T) { assert.Equal(t, 2, requestCounts["/version"]) } +// A failed decode must not break later decodes. Previously the reused json.Decoder +// stayed desynced after one truncated response, breaking decode until restart. +func TestDecodeRecoversFromError(t *testing.T) { + dm := &dockerManager{} + + // truncated JSON: body reads fine, decode fails + var bad []container.ApiInfo + err := dm.decode(&http.Response{Body: io.NopCloser(strings.NewReader(`[{"Id":"abc`))}, &bad) + require.Error(t, err) + + // the next decode must still succeed + var good []container.ApiInfo + err = dm.decode(&http.Response{Body: io.NopCloser(strings.NewReader(`[{"Id":"abcdef012345","Names":["/ok"]}]`))}, &good) + require.NoError(t, err) + require.Len(t, good, 1) + assert.Equal(t, "abcdef012345", good[0].Id) +} + func TestCycleCpuDeltas(t *testing.T) { dm := &dockerManager{ lastCpuContainer: map[uint16]map[string]uint64{