Lesson 5 — Corrections to Lesson 4

What an adversarial review caught when checked against real httpkit source + GCS docs · a production incident
← Back to Lesson 4 · Reference →

Why this lesson exists. Lesson 4 made 7 claims about why the two upload functions differ. We then ran an adversarial subagent whose only job was to disprove each claim using real documentation and source code — not parametric memory. Two claims were wrong. This is the more valuable lesson: plausible-sounding architectural justifications are often post-hoc rationalizations. Verify against the source.

Scorecard

#ClaimVerdict
1Authorization/OTel headers would conflict with GCS validationWRONG
2GCS validates field presence/values, not orderUNVERIFIED
3MIME type must match FormFields["content-type"]CONFIRMED
4Filename from path.Base(FormFields["key"])CONFIRMED
5limit+1 fixes an edge-case bug in the standard pathCONFIRMED
6GCS returns 204/empty body on successWRONG
7httpkit Post() does not buffer the pipe bodyCONFIRMED

Correction 1 — the "header conflict" was fiction

✗ WRONG — no such conflict exists

Lesson 4 said:

"Adding an Authorization header on top would conflict with GCS's own auth validation."

This is false on two counts, both verifiable in the actual httpkit source.

httpkit never auto-adds Authorization

handleOptions in httpkit/request/v2/request.go (in the wrapper source) only sets headers the caller explicitly passed via WithHeader:

// the wrapper source
func handleOptions(req *http.Request, opts *options) {
    if len(opts.headers) > 0 {
        req.Header = opts.headers   // only what the caller passed
    }
    if opts.username != "" || opts.password != "" {
        req.SetBasicAuth(opts.username, opts.password)
    }
}

No Authorization header would ever appear on a GCS request unless we explicitly sent one. The "conflict" scenario can't even arise.

What httpkit DOES inject — and why GCS doesn't care

// the wrapper source — runs on every request
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
// adds: traceparent, tracestate

GCS ignores unknown request headers — traceparent and tracestate are simply not part of its signed policy, so they pass through harmlessly. There is no rejection, no 400, no conflict.

The correct justification: bare http.DefaultClient is used for minimal, explicit header control — sending exactly the headers we specify with no framework-injected OTel tracing noise on a third-party (Google) endpoint. That is a cleanliness/correctness-of-intent choice, not a "GCS will reject it otherwise" requirement.

Correction 2 — GCS does not always return 204

✗ WRONG — depends on success_action_status

Lesson 4 said:

"GCS pre-signed POST returns HTTP 204 or 200 with an empty body on success."

The real GCS behaviour, from the docs, depends on the success_action_status form field:

success_action_statusResponse
absent (default)204 No Content, empty body
200200 OK, empty body
201201 Created, XML body — Bucket, Key, ETag, Location
success_action_redirect set303 redirect to that URL

Our code discards the body unconditionally:

// download_and_upload_reserved.go
io.Copy(io.Discard, resp.Body)
return nil
Why it's safe today — but a latent assumption. The live FormFields we receive contain only key, policy, x-goog-date, content-type, x-goog-algorithm, x-goog-signature, x-goog-credential. No success_action_status → GCS defaults to 204 → discarding the body is correct. But if upload-svc's URL generator ever adds success_action_status=201, we would silently throw away returned metadata. The code is correct by coincidence of the current policy shape, not by design.

What held up — and why it matters

Five claims survived. Two are worth re-stating because the review strengthened the evidence behind them:

Claim 5 — the limit+1 bug is real (confirmed via Go stdlib)

io.LimitReader(r, n) returns exactly n bytes when the underlying reader holds exactly n bytes, then EOF. So in the standard path:

n, _ := io.Copy(fw, io.LimitReader(body, limit))
if n == limit { /* reject */ }   // a file EXACTLY limit bytes is wrongly rejected

The reserved path's limit+1 with n > limit is the correct formulation. This is a genuine latent bug in DownloadAndUploadFile, confirmed not just by reading our code but by the documented semantics of io.LimitReader.

Claim 2 — "field order doesn't matter" is unverified, not proven

◐ UNVERIFIED — documented only that the file must be last

The GCS docs explicitly require the file field to be last. They do not document whether the order of the other (non-file) fields matters. Our code relies on Go's randomized map iteration for those fields and assumes order-independence. No evidence contradicts this in practice, but it is an assumption, not a documented guarantee. Flagging it as "safe because documented" (as Lesson 4 implied) overstated the certainty.

The meta-lesson

Architectural "why" answers are hypotheses until verified. Both wrong claims sounded authoritative and were internally consistent — "auth header conflicts," "GCS returns 204." Each was a reasonable guess that happened to be false. The fix wasn't more careful reasoning; it was going to the source: reading handleOptions, reading the GCS response-code docs. When you write down a justification for a design decision, the honest follow-up question is always: what specific line of code or paragraph of docs proves this? If you can't cite it, mark it as an assumption.