# S3 Downloads Your HTML Instead of Rendering It — The Content-Type Fix

Canonical: https://commareports.com/fix/s3-html-downloads-instead-of-opening
Published: 2026-09-07

> An HTML file uploaded to S3 downloads instead of opening because the object's Content-Type is binary/octet-stream. How to fix it on upload, retroactively, and when to stop using a bucket.

# The browser downloads your S3-hosted report

You upload `index.html`, open the URL, and the browser saves a file
instead of rendering a page. The file is fine — open it locally and it
renders.

## The header, not the file

The browser decides what to do with a response from its
`Content-Type`. S3 stores that as object metadata at upload time and
serves it verbatim. When the uploader cannot infer a type, it writes
`binary/octet-stream`, and the browser's only correct response to an
octet-stream is to save it.

Check what your object actually claims:

```bash
curl -sI https://bucket.s3.amazonaws.com/index.html | grep -i content-type
# content-type: binary/octet-stream     ← this is the bug
```

## Fixing it

**On upload:**

```bash
aws s3 cp report.html s3://my-bucket/ --content-type text/html
```

**For a directory**, the CLI's recursive copy needs a pass per
extension, because `--content-type` applies to everything it touches:

```bash
aws s3 cp dist/ s3://my-bucket/ --recursive \
  --exclude "*.html" --exclude "*.css" --exclude "*.js"
aws s3 cp dist/ s3://my-bucket/ --recursive \
  --exclude "*" --include "*.html" --content-type text/html
aws s3 cp dist/ s3://my-bucket/ --recursive \
  --exclude "*" --include "*.css" --content-type text/css
aws s3 cp dist/ s3://my-bucket/ --recursive \
  --exclude "*" --include "*.js" --content-type application/javascript
```

Miss the CSS pass and you get [a report with no styling](/html-report-broken-css)
instead of a download — the same class of bug wearing a different hat.

**Retroactively**, without re-uploading:

```bash
aws s3 cp s3://b/k s3://b/k --content-type text/html \
  --metadata-directive REPLACE
```

**Terraform:**

```hcl
resource "aws_s3_object" "report" {
  bucket       = aws_s3_bucket.reports.id
  key          = "index.html"
  source       = "dist/index.html"
  content_type = "text/html"   # omit this and you are back at the top
}
```

## Still downloading after the fix

Two more suspects:

1. **`Content-Disposition: attachment`** set on the object, which
   overrides the content type entirely. Same `--metadata-directive
REPLACE` copy, without the disposition.
2. **CloudFront cached the old response.** Invalidate the path —
   `aws cloudfront create-invalidation --distribution-id … --paths "/*"` —
   and remember that the cached copy is what your colleague is getting
   even after your own reload looks fixed.

## The bigger question

Getting one header right is easy. The list of things that come with it
is what to weigh:

- Bucket policy and public-access-block settings, or an OIDC role plus
  signed URLs if the reports are private.
- A lifecycle rule, or the bucket grows forever.
- Per-extension content types on every deploy, forever.
- CloudFront invalidation on every publish.
- No access control per report, unless you build it.
- No place for the discussion the report exists to start.

That is a reasonable amount of infrastructure for a document three
people will read and argue about. The alternative — POST the HTML,
get a URL — has none of it: content types, asset rewriting, per-report
access (private, invited, link, public) and revision history are the
default rather than a checklist. Comparison in
[S3 static hosting alternatives](/alternatives/s3-static-hosting-alternatives).

Keep the bucket for what buckets are good at: artifacts other machines
consume.

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited
revision history.

**[Publish a report and get a URL →](https://commareports.com/)**

### Related

- [S3 static hosting alternatives](/alternatives/s3-static-hosting-alternatives)
- [Host an HTML file](/host-html-file) · [Share an HTML folder](/share-html-folder)
- [My HTML report lost its CSS](/html-report-broken-css)
- [Publish from CI](/docs/ci)
