codecov / codecov/codecov-javascript-bundler-plugins

Bundle stats upload fails against AWS S3 storage (chunked Transfer-Encoding → 501 NotImplemented)

Open Beginner friendly
#351 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
9
Forks
10
PR merge metrics
No merged PRs in 30d

Description

Summary

uploadStats sends the stats payload as a streamed request body (ReadableStream + duplex: "half"), which undici serializes with Transfer-Encoding: chunked (no Content-Length). Amazon S3 does not support chunked PUTs and rejects the pre-signed upload with 501 NotImplemented, so bundle analysis is completely non-functional on any self-hosted Codecov instance whose storage backend is real AWS S3.

MinIO (the common self-hosted default) is lenient and accepts chunked, which is likely why this hasn't surfaced widely — but a Codecov Enterprise instance configured with services.minio.host: s3.amazonaws.com hits it every time.

Environment

  • @codecov/vite-plugin 2.0.1 (@codecov/bundler-plugin-core 2.0.1)
  • Node.js v24.14.1
  • Self-hosted Codecov (Enterprise 25.5.1) with storage backend = AWS S3 (services.minio.host: s3.amazonaws.com)

Root cause

In packages/bundler-plugin-core/src/utils/uploadStats.ts the body is a stream:

const stream = new ReadableStream({
  pull(controller) { /* ... enqueue chars ... */ },
}).pipeThrough(new TextEncoderStream());

await fetchWithRetry({
  url: preSignedUrl,
  requestData: {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    duplex: "half",
    body: stream,   // <-- undici sends Transfer-Encoding: chunked, no Content-Length
  },
});

A streamed body with no known length is sent as Transfer-Encoding: chunked. S3 returns:

<Error>
  <Code>NotImplemented</Code>
  <Message>A header you provided implies functionality that is not implemented</Message>
  <Header>Transfer-Encoding</Header>
</Error>

With a large real payload, S3 responds 501 and closes the socket mid-upload, so undici surfaces it as a low-level TypeError: fetch failed (rethrown from fetchWithRetry) rather than the clean 501 — hence the opaque Failed to upload stats, fetch failed users see. With a tiny payload the full body flushes first and you get the clean 501.

Reproduction

  1. Configure a self-hosted Codecov whose storage is genuine AWS S3.
  2. Run any build with the plugin enabled + CODECOV_TOKEN set, pointing apiUrl at that instance.
  3. Pre-signed URL fetch succeeds; upload-stats fails 3× → Failed to upload stats, fetch failed.

Minimal repro of the underlying HTTP behavior (Node fetch → S3 pre-signed PUT):

// buffered string body -> Content-Length set -> 200 OK
await fetch(preSignedUrl, { method: "PUT", headers: { "Content-Type": "application/json" }, body: statsJson })

// streamed body + duplex:"half" -> Transfer-Encoding: chunked -> 501 NotImplemented
const stream = new ReadableStream({ /* enqueue chars */ }).pipeThrough(new TextEncoderStream())
await fetch(preSignedUrl, { method: "PUT", headers: { "Content-Type": "application/json" }, duplex: "half", body: stream })

Suggested fix

Send the payload as a buffered string/Uint8Array so fetch sets Content-Length, instead of a ReadableStream:

await fetchWithRetry({
  url: preSignedUrl,
  requestData: {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: message,   // string -> Content-Length, S3-compatible
  },
});

The stats JSON is already fully in memory (it's iterated char-by-char to build the stream), so buffering it adds no meaningful memory overhead and makes uploads compatible with S3 as well as MinIO/GCS.

Alternatively, keep streaming but compute and set an explicit Content-Length header, or use S3's aws-chunked content-encoding — but a plain buffered body is by far the simplest and works across all backends.

I've verified the buffered-body change fixes uploads against AWS S3 end-to-end (Successfully uploaded stats). Happy to open a PR if useful.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/bundler-plugin-core/src/utils/uploadStats.ts and inspect how the stats payload is constructed and passed to fetchWithRetry. Verify the upload uses a body with a known length and test the plugin against genuine AWS S3; done means bundle stats upload succeeds instead of returning the chunked-transfer 501 error.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, node.js, typescript
Domain
cloud
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.