Developer reviewing Go LimitReader code and an oversized HTTP response on dual monitors

A Go LimitReader Can Hide an Oversized Response

Fri, Oct 16, 2026

Give io.ReadAll(io.LimitReader(r, 6)) the seven-byte input ABCDEFG. In the supplied qualification, it returns ABCDEF and a nil error. Give it the six-byte input ABCDEF, and it returns the same bytes with the same error result. A client that intended to accept only a complete body of at most six bytes cannot distinguish those inputs from these results alone.

The two functions have not failed their own contracts. The Go io documentation defines a limiter that can introduce EOF and a collector that regards EOF as normal completion. The mismatch is between limiting returned bytes and validating the maximum length of the supplied response.

This playbook qualifies a different contract: read through a ceiling of N+1 bytes, preserve any observed non-EOF read error, and admit data only after a successful read returns at most N bytes. An oversized or failed operation returns nil data, never a prefix presented as the complete response.

The evidence is deliberately small: synthetic ASCII strings, N=6, zero and invalid-limit controls, two injected failures, and one loopback HTTP response whose declared length is unknown. Twelve main case IDs preserve exact returned bytes and error categories. The recorded local run is supplied commissioning evidence, not a production claim. The useful deliverable is a reviewable admission boundary, with explicit limits on what that boundary proves.

Define acceptance before limiting the reader

Write the acceptance rule before choosing the wrapper: the client may expose the complete finite result of the supplied reader only when reading succeeds and the result contains no more than six bytes. The starting position matters. For this laboratory, every operation receives a fresh reader positioned before its first byte.

The literal inputs make the boundary visible. ABCDE contains five bytes and belongs inside the policy. ABCDEF contains six bytes and also belongs inside it. ABCDEFG contains seven bytes and must not be admitted. The accepted value for the second input is the whole ABCDEF; the first six bytes of the third input are not an equivalent response.

Treat diagnostic evidence and accepted data as separate outputs in the design, even when the implementation exposes only one data return. A diagnostic prefix can explain a failure. It must not enter the same downstream path as a complete admitted body. In this fixture, the rejecting helper does not return that prefix at all.

Define the verdict vocabulary accordingly. ACCEPT_BOUNDED approves this byte contract. REJECT_OVERSIZED identifies observed excess. HOLD_READ_FAILURE withholds admission because reading failed. REJECT_CONFIGURATION rejects an invalid cap. REPAIR applies to a candidate implementation or acceptance rule that cannot establish the intended boundary; it does not transform a truncated response into a valid one.

A reviewer should be able to answer two independent questions: did the diagnostic behave as expected, and did the response qualify for admission? A successful negative control can demonstrate precisely why the candidate needs repair.

Broader API input and response validation foundations provide context for declaring explicit checks. Here, however, the body is opaque bytes. Its maximum accepted length is the entire validation subject, not a substitute for other application contracts.

Separate a byte ceiling from evidence of completion

There are two consecutive interfaces to inspect. LimitReader(r, N) exposes a reader with a byte allowance. ReadAll consumes that exposed reader. The LimitReader and ReadAll contracts permit the limiter to stop with EOF at its allowance and the collector to return nil error when it sees EOF. The collector need not discover why the wrapper ended.

The LimitedReader implementation makes the mechanism concrete. Once its remaining allowance is nonpositive, it returns EOF without asking the underlying reader for another byte. Before that point, it restricts the supplied read buffer to the remaining allowance. The wrapper's endpoint can therefore precede the underlying reader's endpoint.

A short individual Read is not the same as a short, completed ReadAll. The Reader contract allows available bytes to be returned without filling the requested buffer. ReadAll continues until an error or EOF; admission depends on that completed operation, not one short read.

That distinction gives three separate ideas: completion of the limited view, completion of the supplied upstream reader, and successful fulfillment of the application's response contract. Do not collapse them into the single phrase “read succeeded.” Even a genuine upstream EOF cannot prove that a service generated every business record it should have generated.

The helper's configuration policy permits N from zero through 1 MiB, inclusive. Because N is an int64 in this narrow range, N+1 cannot overflow. The upper boundary exists to make this laboratory's configuration explicit; it is neither a universal production recommendation nor a measured memory budget.

Name the bytes being limited

The literals are synthetic ASCII bytes. Their convenient printed appearance does not turn this into a character-count policy. The helper tests len(b), not a user-visible text length.

For G12, the supplied reader is response.Body, and the fixture applies no content coding. State that reader layer when interpreting the result. A different integration must separately document the reader actually passed to bounded, including any wrappers before it. This check does not automatically measure every transport representation, constrain an upstream buffer, or certify the completeness of a different layer's output.

Run the twelve-case Go qualification

Use a new writable disposable directory with the commissioning environment: Go 1.23.2 (runtime go1.23.2), Linux, amd64. The program imports only the standard library. It needs the Go toolchain, permission to read its source and write its report, and permission to open one temporary loopback listener. No unpublished helper, external module, or remote API is required.

Save the complete listing as limit_reader_fixture.go, then run exactly:

go run limit_reader_fixture.go -source limit_reader_fixture.go

The -source argument must name the same source file passed to go run. The driver hashes that file; it does not independently attest that an arbitrary path supplied to the flag is the executed source. It records the actual runtime rather than enforcing a compiler-version match. Retain a rerun's own source, command, exit result, and generated report together.

The supplied commissioning observation is PASS_12_CASES, with twelve main case IDs, on go1.23.2, linux, amd64. Its run began at 2026-10-16T06:17:37.541597242Z and ended at 2026-10-16T06:17:37.54400488Z. These timestamps identify that observation; they are not benchmark results. The supplied source SHA-256 is:

194c7a19bf0740e2c7b2febc66524b568fc505eb1556392aeeafde3f19b35e75

The listing below preserves that source. Changes, including source-file formatting changes, require their own digest; any changed execution needs its own evidence. The observations quoted here remain those of the supplied commissioning run. A rerun must be identified by its own report rather than borrowing those timestamps.

// Synthetic reader contract, with one loopback HTTP integration control.
package main
import (
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "errors"
    "flag"
    "fmt"
    "io"
    "net/http"
    "net/http/httptest"
    "os"
    "path/filepath"
    "runtime"
    "strings"
    "time"
)
var oversized = errors.New("oversized")
var badLimit = errors.New("invalid-limit")
var injected = errors.New("injected-read-error")
// Lab configuration cap makes n+1 safe. It is not a measured memory limit.
func bounded(r io.Reader, n int64) ([]byte, error) {
    if n < 0 || n > 1<<20 {
        return nil, badLimit
    }
    b, err := io.ReadAll(io.LimitReader(r, n+1))
    if err != nil {
        return nil, err
    }
    if int64(len(b)) > n {
        return nil, oversized
    }
    return b, nil
}
func naive(r io.Reader, n int64) ([]byte, error) {
    return io.ReadAll(io.LimitReader(r, n))
}
type failReader struct{}
func (failReader) Read([]byte) (int, error) { return 0, injected }
func classify(err error) string {
    switch {
    case err == nil:
        return "none"
    case errors.Is(err, oversized):
        return "oversized"
    case errors.Is(err, badLimit):
        return "invalid-limit"
    case errors.Is(err, injected):
        return "injected-read-error"
    default:
        return err.Error()
    }
}
func must(ok bool, message string) {
    if !ok {
        panic(message)
    }
}
func main() {
    source := flag.String("source", "limit_reader_fixture.go", "source file for digest")
    flag.Parse()
    raw, err := os.ReadFile(*source)
    if err != nil {
        panic(err)
    }
    digest := sha256.Sum256(raw)
    dir, err := os.MkdirTemp(".", "limit-reader-run-")
    if err != nil {
        panic(err)
    }
    rows := []map[string]any{}
    report := map[string]any{"status": "FAIL", "fixture": "limit-reader-v1", "started_utc": time.Now().UTC().Format(time.RFC3339Nano), "go": runtime.Version(), "os": runtime.GOOS, "arch": runtime.GOARCH, "source_sha256": hex.EncodeToString(digest[:])}
    defer func() {
        if e := recover(); e != nil {
            report["failure"] = fmt.Sprint(e)
        }
        report["cases"] = rows
        report["finished_utc"] = time.Now().UTC().Format(time.RFC3339Nano)
        out, e := json.MarshalIndent(report, "", "  ")
        if e != nil {
            panic(e)
        }
        path := filepath.Join(dir, "results.json")
        if e = os.WriteFile(path, append(out, '\n'), 0600); e != nil {
            panic(e)
        }
        fmt.Println(path)
        if report["status"] != "PASS_12_CASES" {
            os.Exit(1)
        }
    }()
    run := func(id string, fn func() ([]byte, error), want, wantErr, decision string) {
        row := map[string]any{"id": id, "status": "FAIL", "decision": decision}
        rows = append(rows, row)
        b, e := fn()
        row["bytes"] = string(b)
        row["length"] = len(b)
        row["error"] = classify(e)
        row["returned_nil"] = b == nil
        must(string(b) == want && classify(e) == wantErr, id+": wrong result")
        if wantErr != "none" {
            must(b == nil, id+": rejected data escaped")
        }
        row["status"] = "PASS"
    }
    for _, c := range []struct{ id, text, want, decision string }{
        {"G1", "ABCDE", "ABCDE", "CONTROL"},
        {"G2", "ABCDEF", "ABCDEF", "INSUFFICIENT_BOUNDARY"},
        {"G3", "ABCDEFG", "ABCDEF", "REPAIR"},
    } {
        run(c.id, func() ([]byte, error) { return naive(strings.NewReader(c.text), 6) }, c.want, "none", c.decision)
    }
    for _, c := range []struct {
        id, text, want, wantErr string
        n int64
    }{
        {"G4", "ABCDE", "ABCDE", "none", 6},
        {"G5", "ABCDEF", "ABCDEF", "none", 6},
        {"G6", "ABCDEFG", "", "oversized", 6},
        {"G7", "", "", "none", 0},
        {"G8", "X", "", "oversized", 0},
    } {
        run(c.id, func() ([]byte, error) { return bounded(strings.NewReader(c.text), c.n) }, c.want, c.wantErr, "BOUNDED_CONTRACT")
    }
    run("G9", func() ([]byte, error) {
        return bounded(io.MultiReader(strings.NewReader("ABC"), failReader{}), 6)
    }, "", "injected-read-error", "HOLD_READ_FAILURE")
    run("G10", func() ([]byte, error) {
        a, e := naive(io.MultiReader(strings.NewReader("ABCDEF"), failReader{}), 6)
        must(string(a) == "ABCDEF" && e == nil, "late-error negative control missing")
        return bounded(io.MultiReader(strings.NewReader("ABCDEF"), failReader{}), 6)
    }, "", "injected-read-error", "REPAIR_AND_HOLD")
    run("G11", func() ([]byte, error) { return bounded(strings.NewReader("X"), -1) }, "", "invalid-limit", "REJECT_CONFIGURATION")
    run("G12", func() ([]byte, error) {
        server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "application/octet-stream")
            w.WriteHeader(200)
            w.(http.Flusher).Flush()
            _, _ = io.WriteString(w, "ABCDEFG")
        }))
        defer server.Close()
        client := server.Client()
        client.Timeout = 2 * time.Second
        response, e := client.Get(server.URL)
        if e != nil {
            return nil, e
        }
        defer response.Body.Close()
        must(response.StatusCode == 200 && response.ContentLength == -1, "HTTP setup changed")
        return bounded(response.Body, 6)
    }, "", "oversized", "REJECT_OVERSIZED")
    must(len(rows) == 12, "case inventory mismatch")
    report["status"] = "PASS_12_CASES"
}

Keep a failing diagnostic case in the report

The report starts with status FAIL, and each case row enters the inventory as FAIL before its operation executes. The runner then records returned bytes, length, error category, and returned_nil. Exact byte and error comparisons must succeed. Every expected-error case additionally requires a nil data return before its row becomes PASS.

After report handling is installed, a recovered panic leaves partial evidence and causes a nonzero exit. Earlier failures, such as an unreadable source or an unwritable working directory, can stop execution before a report exists. A missing toolchain also prevents execution. Never interpret an absent report as an empty successful run.

The driver creates a limit-reader-run- directory and prints the path to its results.json. Success requires the twelve-row check, the final PASS_12_CASES status, and successful report writing. A failure while serializing or writing the report is not a successful evidence handoff, even when earlier assertions passed.

Inputs and limits live in the preserved source rather than separate JSON fields. Join case IDs to that source; do not infer an original input solely from returned bytes. The row's decision string is an assigned case label, not an independently measured admission result. In particular, BOUNDED_CONTRACT groups both accepted and rejected controls.

The primary documentation was checked on October 16, 2026. The supplied packet recorded go1.26.5 as an io documentation-page identity, separate from the tested runtime and not a claim about the page's current or latest version. The io and httptest documentation URLs, and the standard-library source URL, are moving references rather than pinned compiler evidence. The net/http reference is explicitly versioned go1.23.2. Keep those source identities distinct from the run's own runtime and timestamps.

Compare short, exact and oversized naive reads

G1 establishes the short-input control. The naive operation receives ABCDE with N=6 and returns those five bytes without error. This is the intended complete input, but it is only the easy side of the boundary. A helper that behaves correctly on a short fixture has not yet shown that it can reject a longer one.

G2 supplies ABCDEF. The returned value is ABCDEF, length six, with error category none. Since the source is known in the fixture, the reviewer knows that this particular value is complete. The naive result itself does not convey that knowledge. Its case label, INSUFFICIENT_BOUNDARY, identifies the missing distinction.

G3 supplies ABCDEFG. It also returns ABCDEF, length six, with error category none. This is the failing operational case: the candidate has returned a permitted-size prefix of a prohibited-size input. A caller checking only err == nil and len(b) <= N would approve it.

The equality between G2 and G3 is the evidence to retain. Both source literals and both returned values matter. A report reduced to “received six bytes” loses the very difference the test was constructed to expose. Increasing the chosen N without changing the admission rule merely relocates this ambiguity.

The mechanism follows the standard-library limiter source: at the allowance boundary, the wrapper can stop before the underlying reader supplies anything further. There is no obligation for this wrapper to diagnose oversized input. Using a byte ceiling as a completeness test asks it to provide a different contract.

G3 therefore passes as a diagnostic while its implementation receives REPAIR. Keep that distinction visible in review comments and automated summaries.

The neighboring health-response contract checks address a different falsely successful signal. This fixture does not repeat endpoint or content checks. It holds the body opaque and asks whether the complete supplied byte sequence fits the client's cap.

Read one extra byte and reject the excess

The repaired operation keeps the configured acceptance limit at six but lets the collector observe up to seven bytes. G4 returns ABCDE with nil error. G5 returns ABCDEF with nil error. Both qualify for ACCEPT_BOUNDED because the probe completes successfully before exhausting its seven-byte allowance.

G6 supplies ABCDEFG. Seven observed bytes are sufficient to disprove an at-most-six-byte contract. The helper returns nil, oversized. It does not return ABCDEF, does not remove the extra byte and call the result repaired, and does not expose any other rejected prefix through its data return.

The extra byte is evidence, not accepted payload. For an input longer than seven bytes, the same observation would establish excess without determining its total length. That is a derivation from this helper's comparisons, not an additional case in the supplied run. A size rejection needs evidence that the cap was crossed, not a complete inventory of the remaining source.

Be precise about the ceiling. N+1 bounds the bytes passed through the limiter into the collected result. It is not an N+1 allocation guarantee: the ReadAll implementation manages its own storage. Nor does the ceiling bound the number of read attempts, elapsed time, or work performed beneath the supplied reader. No allocation or performance claim follows from this fixture.

Do not read N bytes and guess from equality

At N bytes, an exactly sized input and a longer input remain indistinguishable under the naive wrapper. Reading through an allowance of N+1 creates room to observe either termination, a real error, or one byte of excess.

The helper preserves that ordering: it handles a returned read error first, then checks excess, and only then returns accepted data. Its successful at-most-N branch has not reached the artificial N+1 boundary. Subject to the supplied reader's contract, that is the completion evidence missing from G2's naive result alone.

Handle a zero-byte policy without special pleading

N=0 has a meaningful application interpretation: admit an empty completed reader and reject any reader that exposes a byte. It is not a request to disable validation. G7 and G8 show that the same N+1 logic can enforce this policy without a separate successful shortcut.

For G7, bounded probes the empty string reader with an allowance of one. Reading finishes successfully with zero bytes, so the empty result qualifies for ACCEPT_BOUNDED. Its empty content is intentional. The admission decision comes from successful completion plus the length comparison, not from treating every empty-looking value as success.

For G8, the input is X. The one-byte allowance permits that byte to be observed; one exceeds the acceptance limit of zero. The helper returns nil data and the oversized error. The report renders both an accepted empty result and a nil rejected slice as an empty string. The error category distinguishes the decisions; returned_nil separately checks that rejection exposed no data.

Contrast this with immediately returning an empty value for every zero limit. Such a shortcut could not distinguish G7 from G8 because it would never inspect the source. It would define “ignore the body,” not “accept only a complete empty body.” That reasoning does not require adding a new experiment to the twelve-case inventory.

G11 addresses a different condition: N=-1. The helper rejects configuration before attempting to read the supplied X. The result is nil, invalid-limit, mapped to REJECT_CONFIGURATION rather than a response-size verdict. A negative cap must not acquire accidental product meaning through a lower-level wrapper.

The same guard rejects values greater than 1 MiB, as visible in the preserved source. The supplied inventory does not exercise that upper rejection or the largest permitted cap; do not report those as additional passing controls. Changing the configured range requires separate review of the policy and the safety of N+1, not silent removal of the guard.

Preserve a real read failure

G9 supplies a logical sequence consisting of ABC followed by failReader. The second component returns injected-read-error, not EOF. Three bytes have been available, but successful completion has not been established. Being shorter than the cap cannot turn this failed read into an acceptable complete body.

The io.Reader error contract permits data and a non-nil error to be returned by the same call, or an error to follow data on a later call. ReadAll processes the available bytes; bounded then applies the higher-level admission rule. Handling bytes correctly at the reader interface does not require publishing them as an accepted response.

Here, the helper's first check after ReadAll is err != nil. It returns nil data and the original error. G9 therefore records empty returned bytes, length zero, injected-read-error, and returned_nil=true. Its operational decision is HOLD_READ_FAILURE. The report does not say that the source contained zero bytes; it says that no data escaped through the rejecting helper's return.

This precedence also matters when reasoning about a read that supplies excess bytes together with a non-EOF error. The error branch would still win. That behavior follows from the source order; G9 is not a test of every possible simultaneous data-and-error combination. Do not relabel such an observed failure as a clean size rejection merely to simplify reporting.

A discarded return value does not undo consumption

By the time bounded rejects G9, the underlying sequence has advanced. Returning nil changes what the caller receives, not what the source has already delivered. The helper offers no rollback, rewind, or transaction across reader operations.

Keep that distinction in the caller's ownership rules. A failure path may withhold the body from its intended consumer, but it cannot retroactively undo earlier consumption elsewhere. Describe nil as a no-data admission result, not as proof that the source was untouched or that partial bytes were erased from every intermediate buffer.

Probe the error hidden just beyond the naive boundary

G10 makes the ambiguous exact-boundary result more demanding. Its source is six bytes, ABCDEF, followed by the failing reader. The naive subcontrol executes first and must return ABCDEF with nil error. If that expected negative control disappears, the case panics rather than silently proceeding to the repaired comparison.

Why can this appear successful? After six bytes, the naive allowance is exhausted. The wrapper supplies EOF before the next component's failure needs to be encountered. The result looks like G2 even though this sequence has not ended successfully.

The second operation constructs a fresh io.MultiReader over a fresh string reader and failReader. It is not a continuation of the first consumed reader. Reconstructing the source preserves the comparison: both candidates start before the same literal bytes and face the same later error condition.

With an allowance of seven, the bounded probe can progress beyond the six-byte prefix. The MultiReader contract describes sequential reading of its components and propagation of a non-EOF error. Here the injected error reaches bounded, which returns nil data and preserves its category. The main G10 row records that bounded result, not a second successful prefix.

The source label REPAIR_AND_HOLD summarizes two judgments. Repair the naive acceptance candidate that concealed the failure. Hold admission of the bounded operation because a real read failure remains. A passing G10 confirms both assertions in their specified order; it does not convert the source into a valid response.

Do not extend this into a promise to discover every future error. Once N+1 bytes have been observed successfully, excess is already established and a later underlying error may remain unvisited. Error precedence applies to an error actually returned by the bounded read, not to all possible failures beyond its observation window. The helper qualifies admission; it does not exhaustively diagnose an unbounded source.

Apply the rule to an unknown-length HTTP body

G12 carries the same helper into an actual local HTTP response. The httptest server documentation describes NewServer's loopback listener and the caller's cleanup responsibility. The fixture creates one such server, obtains its client, and closes the server when the case finishes. This is an integration control, not evidence of deployment coverage.

The handler sets application/octet-stream, writes status 200, flushes the headers, and then writes ABCDEFG. It does not apply compression. The client receives the response and asserts both StatusCode == 200 and ContentLength == -1 before calling bounded(response.Body, 6).

That assertion protects the experiment's premise. Unknown declared length is not inferred from the absence of a convenient number in a screenshot; it is checked in the running driver. The versioned net/http response contract identifies -1 as unknown length. If the setup no longer produces that state, G12 must fail rather than quietly become a different test.

The admission operation does not use the declared length. It observes seven bytes through the supplied body reader, returns nil data with oversized, and assigns REJECT_OVERSIZED. The supplied passing run establishes this result for that one local setup. It does not establish the total behavior of unrelated servers or reader wrappers.

Status 200 is a different check

Status and size answer different questions. G12 deliberately fixes status at 200 so that the byte decision cannot be attributed to a status rejection. Passing the status assertion leaves the maximum accepted length undecided until the body operation runs.

Likewise, the handler's media-type declaration is setup context, not an additional content-validation gate in this helper. The payload stays opaque. There is no endpoint matrix and no attempt to infer application correctness from these seven letters.

The client sets a two-second timeout, and it defers response.Body.Close() after a successful Get. Those responsibilities surround the bounded read without changing its decision rule. A failed Get returns its error, while a body-read failure would not satisfy G12's expected oversized result. Neither failure should be rewritten to preserve a green integration test.

Keep deadlines and body ownership separate

The extra read is necessary for the boundary, but necessity does not guarantee that it finishes. Consider a supplied reader that exposes exactly six bytes and then stalls without producing EOF or an error. The helper still needs completion evidence. It cannot accept those six bytes merely because waiting longer is inconvenient.

G12's timeout is a separate lifetime control. The Client.Timeout documentation includes response-body reading in the client's time limit and notes that the timer continues after Get returns. The fixture's two-second value is a laboratory setting, not a recommended service deadline or a measured latency expectation.

If a relevant read fails because its deadline is reached, this admission policy withholds the body. A timeout does not mean “the body probably ended at N.” It means the successful bounded-read prerequisite was not established. The fixture does not inject a timeout, so no timeout outcome belongs in its passing case count.

Ownership is independent as well. The Response.Body ownership contract places closing responsibility on the caller. The deferred close in G12 remains in place whether the body is admitted or rejected. Closing is cleanup, not proof that the body passed its cap, and the fixture does not assert a Close error result.

The related discussion of stream ownership and pool timeouts concerns a different lifecycle problem. Keep that concern separate rather than adding a pool experiment here.

Finally, bounded accepts an io.Reader, not a universal deadline mechanism. Its byte allowance does not make hostile, broken, or indefinitely stalled reader implementations safe. Caller-owned lifetime controls and an understood reader contract remain prerequisites. This small helper's responsibility is to avoid admitting a successful-looking truncated prefix, not to provide every protection an API client might require.

Reconcile the twelve outcomes

The table joins source inputs to returned evidence and operational decisions. A nil data return has zero length but is not an accepted empty body. Error category none means no returned error; it does not independently mean policy acceptance.

Case

Input / reader

N

Returned data

Length

Error

Operational decision

G1

ABCDE

6

ABCDE

5

none

Control; candidate
unqualified

G2

ABCDEF

6

ABCDEF

6

none

REPAIR
(ambiguous boundary)

G3

ABCDEFG

6

ABCDEF

6

none

REPAIR

G4

ABCDE

6

ABCDE

5

none

ACCEPT_BOUNDED

G5

ABCDEF

6

ABCDEF

6

none

ACCEPT_BOUNDED

G6

ABCDEFG

6

nil

0

oversized

REJECT_OVERSIZED

G7

Empty reader

0

Empty data

0

none

ACCEPT_BOUNDED

G8

X

0

nil

0

oversized

REJECT_OVERSIZED

G9

ABC, then error

6

nil

0

injected-read-error

HOLD_READ_FAILURE

G10

ABCDEF,
then error

6

nil

0

injected-read-error

REPAIR +
HOLD_READ_FAILURE

G11

X (not read)

-1

nil

0

invalid-limit

REJECT_CONFIGURATION

G12

HTTP body:
ABCDEFG

6

nil

0

oversized

REJECT_OVERSIZED

G10 has one main row and an asserted naive subcontrol. Its first operation returns ABCDEF, length six, with no error; its recorded main result is the bounded failure shown above. G12 is the sole loopback integration control. These are twelve main cases, not thirteen independent result rows.

For G1, the short naive control returns the correct complete literal, but the candidate's boundary remains unqualified. For G2, the known six-byte source fits while the returned evidence alone is ambiguous. REPAIR therefore concerns the candidate admission path, not a claim that those known six bytes are oversized. G3 demonstrates its actual prefix-admission defect.

The source's CONTROL, INSUFFICIENT_BOUNDARY, BOUNDED_CONTRACT, and REPAIR_AND_HOLD strings remain unchanged in the JSON labels. The operational column interprets them; it does not replace the literal oracle. In particular, G4, G5, and G7 are accepted, while G6 and G8 are rejected despite sharing BOUNDED_CONTRACT labels.

Reconcile the table against the supplied go1.23.2 Linux amd64 observation and a retained source identity, not against a documentation version label. A new runtime or changed source requires its own recorded outputs. The fixture records lengths, while its exact string assertions already constrain those lengths for these literals.

For complementary request-and-assertion practice, repeatable API response testing provides broader context. This Go report remains the evidence for the reader boundary; a different testing interface does not substitute for its byte and error assertions.

Recover without treating a prefix as the original response

Recovery starts by preserving the decision that actually occurred. An oversized result is not a smaller complete response, and a read failure is not successful completion with a warning attached. Do not let a later presentation layer erase those distinctions by displaying only an empty string or a truncated diagnostic value.

For this synthetic laboratory, retain the source, digest, case identity, error category, and report together. The rejected helper return is nil; the driver does not retain a rejected internal prefix. Its source literals supply the controlled input evidence. Avoid describing the report as containing rejected payload bytes that it never records.

In an integration that separately retains diagnostic content, keep it outside the accepted-data path and subject it to the application's retention and access policy. That is an evidence-handling responsibility, not permission to expose the prefix as a valid response. Preserve enough provenance to identify the operation, reader layer, configured cap, and actual failure.

Do not call the helper again on the partly consumed source and treat the result as a fresh complete body. Its starting position has changed. A suffix can fit the cap while saying nothing about whether the original response qualified. The original decision remains rejected or held.

Where the application permits recovery, it requires a new independently bounded operation with trustworthy provenance. This article does not prescribe a universal network retry, a suffix request, or a reconstruction protocol. The integration owner must decide whether a new operation represents an acceptable replacement for the original one.

Do not infer missing content from retained bytes

The literal ABCDEF cannot identify which longer source produced it. G2 and G3 already show that identical returned bytes can accompany different source lengths; many other sources could share the prefix. No responsible recovery procedure can fabricate the omitted portion from that evidence.

Nor does a rejected return undo any action taken by an earlier consumer. The intended arrangement is to place admission before the downstream use that depends on a complete body. Where previous consumption has already occurred, record that limitation rather than claiming rollback. Recovery must preserve the difference between new evidence and a rewritten account of the failed operation.

Make the response-admission decision reviewable

Review the helper as a small state machine with explicit evidence requirements, not as a boolean “size check.” Configuration validity comes first. A real read error comes before a size comparison. Only a successful bounded read with length at most N enters the accepted-data branch. The following matrix separates a response verdict from an implementation repair decision.

Decision

Required evidence

Data handling / action

ACCEPT_BOUNDED

Valid N; N+1-limited ReadAll returns nil error and at most N bytes.

Expose the complete result of the supplied reader at its declared starting point.

REJECT_OVERSIZED

Valid N; N+1-limited ReadAll returns nil error and more than N bytes.

Return nil data with oversized. Do not publish a trimmed prefix.

REPAIR

Candidate returns a permitted-size prefix without establishing the required boundary.

Replace the admission rule and requalify it. Do not relabel a diagnostic PASS as acceptance.

REJECT_CONFIGURATION

N is negative or exceeds the laboratory configuration range.

Reject before reading. Resolve the cap policy separately from the response.

HOLD_READ_FAILURE

Valid N; ReadAll returns an underlying non-EOF read error.

Return nil data and preserve the error, even when some bytes were read.

Client maintainers own the helper, its nil-on-error contract, the selected limit, and the call sites that might otherwise expose rejected data. Integration owners define the supplied reader's starting point and what its complete output represents. QA reviewers own the fixed case inventory and the evidence needed to detect regression. None of those roles can replace the others by pointing to PASS_12_CASES alone.

Use the broader principle of reviewable automated test evidence: retain expectations and observed outcomes in a form another reviewer can inspect. For this boundary, that means the literal source, digest, runtime, timestamps, report, and a clear mapping from diagnostic labels to operational decisions.

A review should explicitly distinguish exercised behavior from source inspection. The upper configuration guard is present but not boundary-tested here. Simultaneous excess bytes and a read error are covered by branch-order reasoning, not a dedicated main case. The single loopback response is not a transport compatibility matrix. These limits belong in the decision, not in an implied claim of complete coverage.

Reopen qualification when reader wrappers, byte-layer assumptions, limit configuration, or downstream admission paths change. Preserve the original failing controls and add separately identified evidence where the changed contract requires it. Do not make G3 green operationally by changing its expected prefix to a newly preferred answer; its diagnostic purpose is to retain the demonstrated defect.

The reviewable outcome is narrow but useful: an identified operation either supplies an admitted bounded result or supplies no accepted data with an explicit reason. A passing diagnostic remains evidence about that rule, not permission to reinterpret a rejected response.

Build stronger API error-handling foundations

The central result is a boundary, not a larger truncation allowance. At N=6, the naive operation cannot distinguish the exact body ABCDEF from the prefix of ABCDEFG. The bounded operation admits successful five- and six-byte results, rejects observed excess, and withholds data when reading fails. Zero and invalid configuration receive explicit treatment rather than accidental semantics.

Keep the result attached to its evidence: a preserved driver, twelve main cases, the supplied runtime record, and the limited meaning of each verdict. The helper does not certify application records, erase prior consumption, or replace lifetime and ownership controls. Those responsibilities remain with the surrounding integration.

For broader API foundations, explore Refonte Learning's APIs Developer Fundamentals. Its published subjects include API documentation and testing, and error handling and logging. Those foundations complement this review discipline; they are not a promise that the programme teaches Go, io.LimitReader, or this specific laboratory.