Skip to content

test(repro): minimal Workers reproducer for the streamed-PUT failures - #203

Draft
alukach wants to merge 1 commit into
mainfrom
repro-streamed-put
Draft

test(repro): minimal Workers reproducer for the streamed-PUT failures#203
alukach wants to merge 1 commit into
mainfrom
repro-streamed-put

Conversation

@alukach

@alukach alukach commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Standalone reproducer under tests/repro/streamed-put-io-context/. Not wired into CI — it is a manual tool and an artifact to hand upstream.

What it isolates

No S3, no auth, no registry. One variable: whether the inbound body stream is touched before or after an await.

  • /before — attach the stream in the request's own I/O context. Mirrors ProxyGateway::op_needs_buffered_body, the guard that covers the multipart control ops and batch delete.
  • /after — await first (standing in for the registry lookup and STS exchange), then attach. Mirrors WorkerBackend::forward for PutObject/UploadPart, which the classifier explicitly excludes.

forward() copies WorkerBackend::forward faithfully, including the FixedLengthStream wrapper and its dropped pipe_to promise.

Result

1 failure in 32 on /after, 0 in 32 on /before, at concurrency 8 × 16 MiB:

[after] forward failed: Error: Network connection lost.

Intermittent — a later 96/96 run was clean. A green run is inconclusive.

Scope

Network connection lost maps to BackendError503, the minority prod mode (2 of 23 in one sample). It does not reproduce the majority 520.

Prod evidence now shows the 520 is relayed from the backend fetch: GatewayResponse::Forward passes the upstream status through verbatim, and the worker's own 5xx log (from #201) records status=520 with handle_request returning normally. tessera's backend is plain S3 in us-west-2, which does not emit 520 — so the runtime is synthesizing it for a subrequest the worker issued. That remains open.

The accompanying TypeError: Can't read from request stream after responding with an exception fires ~1 ms after the response commits — the orphaned pipe_to still reading the inbound body. A consequence rather than the cause, but it is what resets the connection so the client never sees the relayed status.

🤖 Generated with Claude Code

Isolates the write path with no S3, no auth and no registry, leaving one
variable: whether the inbound body stream is touched before or after an await.
The post-await arm mirrors WorkerBackend::forward for PutObject/UploadPart;
the pre-await arm mirrors the op_needs_buffered_body guard.

Reproduces the 503 branch (1 failure in 32 post-await, 0 in 32 pre-await,
"Network connection lost" -> BackendError -> 503). Intermittent: a later
96/96 run was clean, so a green run is inconclusive.

Does not reproduce the majority 520 branch. Prod evidence now shows that one
is relayed from the backend fetch rather than the edge killing the worker --
Forward passes the upstream status through verbatim and the worker's own 5xx
log records status=520 with handle_request returning normally. See the README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 1m 6s —— View job


Reviewing PR #203

  • Gather context (PR body, diff, changed files)
  • Read changed files
  • Analyze for correctness/security/best-practice issues
  • Post final review

✅ No blocking issues — safe to merge.

This is a self-contained, non-production reproducer (isolated under tests/repro/streamed-put-io-context/, not wired into CI, build artifacts gitignored), and it's well-constructed for its purpose: the /before vs /after split faithfully isolates the one variable (timing of body-stream attachment relative to an await) and forward() mirrors WorkerBackend::forward closely enough (including the dropped pipe_to promise) to be a credible upstream artifact.

A couple of very minor, non-blocking observations:

  • tests/repro/streamed-put-io-context/drive.py:36 — the except Exception as e: branch doesn't close conn, so a failed/reset connection's socket isn't explicitly released. Harmless for a short-lived manual driver script, but worth a finally: conn.close() if this gets reused for longer soak runs.
  • tests/repro/streamed-put-io-context/src/lib.rs:61text(599, &msg) uses a non-standard HTTP status (599) purely as an internal sentinel for "the worker crate surfaced an Err"; it's commented as intentional and is within the Fetch spec's valid response-status range, so it's fine, just worth knowing it's not a real status code if anyone greps logs for it later.
  • .gitignore excludes Cargo.lock for what is effectively a standalone binary-ish crate — normally you'd commit the lockfile for reproducibility, but given this is explicitly a throwaway/manual tool (per the Cargo.toml comment "deliberately not a member of the parent crate's build"), that's a reasonable call, not a defect.

Nothing here rises to a correctness bug or security issue — the harness only talks to 127.0.0.1, has no auth/secrets, and the intentional "bug-mirroring" behaviors (dropped promise, ignored slow_call error on /before) are clearly documented as deliberate.

@github-actions

Copy link
Copy Markdown

🚀 Latest commit deployed to https://source-data-proxy-pr-203.source-coop.workers.dev

  • Date: 2026-07-28T19:44:59Z
  • Commit: d40fa84

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant