anomalyco/sst · error

failed to upload assets: HTTP %d %s

Error message

failed to upload assets: HTTP %d %s

What it means

Cloudflare's Workers Assets upload API responds 201 Created when all buckets are uploaded and 202 Accepted when more buckets remain. Any other status is treated as a failed upload and this error includes the HTTP status code plus the response body for diagnosis. It indicates Cloudflare rejected the request — usually auth, account, or payload problems.

Source

Thrown at pkg/server/resource/cloudflare-worker-assets.go:283

	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "multipart/form-data; boundary="+writer.Boundary())
	req.Header.Set("Authorization", "Bearer "+jwt)

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	// API returns:
	// - 202 Accepted if there are more buckets to upload
	// - 201 Created if all buckets have been uploaded
	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
		responseBody, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("failed to upload assets: HTTP %d %s", resp.StatusCode, string(responseBody))
	}

	// Decode response
	var result struct {
		Result UploadResponse `json:"result"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", err
	}

	return result.Result.Jwt, nil
}

View on GitHub (pinned to a0bd20f762)

Solutions

  1. Read the responseBody in the error — Cloudflare includes an errors[] array with codes/messages
  2. Verify the API token is valid and has Workers Scripts/Assets edit permission for the account
  3. Confirm accountId matches the account the worker/assets upload session was created in
  4. Check asset hashes match what the completion session expects (recompute and re-upload changed files)
  5. For 202 responses ensure the caller continues uploading remaining buckets until 201
  6. Retry after a delay on 429/5xx, respecting Cloudflare rate limits

Example fix

// before
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
	responseBody, _ := io.ReadAll(resp.Body)
	return "", fmt.Errorf("failed to upload assets: HTTP %d %s", resp.StatusCode, string(responseBody))
}
// after
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
	responseBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return "", fmt.Errorf("cloudflare auth rejected asset upload (HTTP %d): check API token and permissions: %s", resp.StatusCode, string(responseBody))
	}
	return "", fmt.Errorf("failed to upload assets: HTTP %d %s", resp.StatusCode, string(responseBody))
}
Defensive patterns

Strategy: retry

Validate before calling

// before upload: verify token and account
req, _ := http.NewRequest("GET", "https://api.cloudflare.com/client/v4/accounts/"+accountId, nil)
req.Header.Set("Authorization", "Bearer "+apiToken)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
	return fmt.Errorf("cloudflare token/account invalid before asset upload (HTTP %v)", resp.StatusCode)
}

Type guard

func isUploadAccepted(status int) bool {
	return status == http.StatusCreated || status == http.StatusAccepted
}

Try / catch

resp, err := http.DefaultClient.Do(req)
if err != nil {
	return "", fmt.Errorf("asset upload request failed: %w", err)
}
if !isUploadAccepted(resp.StatusCode) {
	responseBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode == 429 || resp.StatusCode >= 500 {
		// retry with backoff
	}
	return "", fmt.Errorf("failed to upload assets: HTTP %d %s", resp.StatusCode, string(responseBody))
}

Prevention

When it happens

Trigger: POST to /client/v4/accounts/{accountId}/workers/assets/upload?base64=true returns a status other than 201 or 202 (pkg/server/resource/cloudflare-worker-assets.go:283). Typical statuses: 401/403 (bad or missing API token, token lacking Workers Scripts Edit), 404 (wrong accountId or assets upload session not initialized), 400/413 (malformed multipart body, hash mismatch, or payload too large per-bucket limits).

Common situations: Expired or revoked Cloudflare API token; token missing the Workers Assets permission; CLOUDFLARE_ACCOUNT_ID mismatch with the token's account; uploading more assets than fit in one bucket without following the 202 continuation; changed upload-session metadata causing hash mismatch.

Related errors


AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30). Data as JSON: /api/errors/da53a34f03742a1e. Report an issue: GitHub.