cloudflare / cloudflare/cloudflare-typescript

workers.assets.upload.create() drops per-file Content-Type — assets served with empty MIME type

Open
#2,727 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
814
Forks
190
Avg merge
1h 38m
Merged PRs (30d)
2

Description

## Description

`client.workers.assets.upload.create()` accepts `body: Record` (hash → base64 content). When these plain strings are appended to FormData via `addFormValue()` in `src/uploads.ts`, no `Content-Type` header is set per multipart part.

Per Cloudflare's [direct upload docs](https://developers.cloudflare.com/workers/static-assets/direct-upload/):

> "The provided Content-Type header of each file part will be attached when eventually serving the file."

This means assets uploaded via the SDK are served with an empty MIME type, causing browsers to reject JS module scripts:

```
Failed to load module script: Expected a JavaScript-or-Wasm module script
but the server responded with a MIME type of "".
Strict MIME type checking is enforced for module scripts per HTML spec.
```

## How wrangler handles this

Wrangler wraps each file in a `File` object with the correct MIME type before appending to FormData (`packages/wrangler/src/assets.ts`):

```typescript
payload.append(
hash,
new File([base64Content], hash, { type: getContentType(absFilePath) ?? "application/null" }),
hash
);
```

The SDK's `addFormValue()` in `src/uploads.ts` already supports `Uploadable` types (File/Blob) and preserves their `.type` property. But the typed interface for `UploadCreateParams.body` is `Record`, making it impossible to pass File objects without casting.

## The SDK's own example has this bug

`examples/workers/script-with-assets-upload.ts` passes plain strings in the body — assets deployed using this example will also be served with empty MIME types.

## Reproduction

```typescript
import Cloudflare from 'cloudflare';

const client = new Cloudflare({ apiToken: '...' });

// Step 1: Create upload session (works fine)
const session = await client.workersForPlatforms.dispatch.namespaces.scripts.assetUpload.create(
namespace, scriptName, { account_id, manifest }
);

// Step 2: Upload assets — THIS DROPS MIME TYPES
for (const bucket of session.buckets) {
const body: Record = {};
for (const hash of bucket) {
body[hash] = fileContent.toString('base64');
}
await client.workers.assets.upload.create(
{ account_id, base64: true, body },
{ headers: { Authorization: `Bearer ${session.jwt}` } },
);
}

// Step 3: Deploy worker (works fine)
// ... but all assets are now served with empty Content-Type
```

## Expected behavior

The SDK should either:
1. Accept `Record` for the body parameter, allowing users to pass File objects with MIME types
2. Or accept a manifest/path mapping so the SDK can infer MIME types from file extensions (like wrangler does)

## Workaround

Use raw `fetch()` with `FormData` + `Blob` for the upload step (matching wrangler/vibesdk):

```typescript
const formData = new FormData();
for (const hash of bucket) {
const blob = new Blob([base64Content], { type: mimeType });
formData.append(hash, blob, hash);
}
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/assets/upload?base64=true`,
{ method: 'POST', headers: { Authorization: `Bearer ${jwt}` }, body: formData }
);
```

## Environment

- SDK version: `cloudflare@5.2.0`
- Runtime: Bun 1.x (also reproducible with Node.js)
- Also affects Workers for Platforms asset uploads via `workersForPlatforms.dispatch.namespaces.scripts.assetUpload`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.