API Call Response Size Enforcement#
Kyverno's engine limits how many bytes it will read from an HTTP response body during a service context.apiCall. The cap is set via APICallConfiguration.maxAPICallResponseLength β an int64 field configured at startup (e.g., via admission controller flags). A value of 0 disables the check.
How It Works (Current)#
The enforcement lives in executeServiceCall inside pkg/engine/apicall/executor.go. After the HTTP response is received, the executor wraps resp.Body with an io.Reader before reading:
- If
maxAPICallResponseLength == 0: readsresp.Bodydirectly. - If
maxAPICallResponseLength != 0: wraps withio.LimitReader(resp.Body, maxAPICallResponseLength+1)β reads at mostlimit+1bytes.
After io.ReadAll, the body length is checked explicitly: if int64(len(body)) > maxAPICallResponseLength, the executor returns the error "response length must be less than max allowed response length of <limit>" β no panic, no reliance on error types.
The Bug: http.MaxBytesReader + nil ResponseWriter#
Prior to PR #16859, the code used http.MaxBytesReader to enforce the limit :
var w http.ResponseWriter // nil β never assigned
if a.config.maxAPICallResponseLength != 0 {
resp.Body = http.MaxBytesReader(w, resp.Body, a.config.maxAPICallResponseLength)
}
http.MaxBytesReader is designed for server-side request handlers, where it writes Connection: close and 413 Request Entity Too Large headers through the ResponseWriter when the limit is hit. In a client-side context there is no valid ResponseWriter, so w was declared but never assigned β remaining nil. When a response body exceeded the limit, the reader attempted to call w.Header().Set(...) and w.WriteHeader(...) on the nil interface, causing a panic: runtime error: invalid memory address or nil pointer dereference in the engine worker goroutine.
The downstream http.MaxBytesError type-assertion that was supposed to catch this case was therefore never reached safely.
The Fix: io.LimitReader + explicit length check#
PR #16859 replaced http.MaxBytesReader with io.LimitReader, which has no dependency on a ResponseWriter. The limit+1 trick allows the read to complete without an error, after which a simple len(body) > limit comparison determines whether to reject the response. This approach:
- Eliminates the nil pointer panic entirely.
- Removes the
http.MaxBytesErrortype assertion, simplifying error handling. - Works symmetrically for both the error-body path (non-2xx) and the success path.
New unit tests Test_ExecuteServiceCall_MaxResponseLengthExceeded and Test_ExecuteServiceCall_WithinMaxResponseLength were added to pkg/engine/apicall/executor_test.go to cover both branches.
Key Files#
| File | Purpose |
|---|---|
pkg/engine/apicall/executor.go | executeServiceCall β core enforcement logic |
pkg/engine/apicall/config.go | APICallConfiguration β holds maxAPICallResponseLength and timeout |
| Issue #16858 | Bug report with full root-cause analysis |
| PR #16859 | Fix: io.LimitReader replacement and unit tests |