fetch は正しい。認証情報も正しい。同じオブジェクトは curl なら問題なく落ちてくる。それでもブラウザがレスポンスを JavaScript に渡さないのは、バケットが「渡してよい」と言っていないからです。以下の内容は 2026 年 9 月に Amazon S3 ユーザーガイド、AWS CLI リファレンス、MDN で確認しました。Cloudflare 側の同じ問題は R2 の CORS エラーにあり、違いは別ページを読む価値があるほど実在します。
ブラウザは曖昧、S3 は具体的
コンソールに出るのは一般的な失敗です。Chrome はこう書きます。
text
Access to fetch at 'https://my-bucket.s3.eu-west-1.amazonaws.com/key.png'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.Firefox の文言は違います。MDN は「Reason: CORS header 'Access-Control-Allow-Origin' missing」として記録し、「The response to the CORS request is missing the required Access-Control-Allow-Origin header, which is used to determine whether or not the resource can be accessed by content operating within the current origin.」と説明しています。どちらも「無い」ことしか言っておらず、無い理由は複数あります。
S3 のほうが役に立ちます。区別できる 2 つのエラーを返すからです。1 つめ。
text
HTTP/1.1 403 Forbidden
CORS Response: CORS is not enabled for this bucket.これはバケットに CORS 設定が存在しないという意味です。2 つめ。
text
HTTP/1.1 403 Forbidden
CORS Response: This CORS request is not allowed.設定はあるが、リクエストと一致しないという意味です。AWS はこの理由を 3 つに絞っています。「Origin is not allowed」「Methods are not allowed」「Requested headers are not allowed」。どちらの文言もブラウザのコンソールには出ません。失敗したレスポンスの本文はブラウザが隠すからです。curl なら見えます。だから後半のテストが最短の答えになります。
| 症状 | S3 の言い分 | 直す要素 |
|---|---|---|
CORS is not enabled for this bucket | 設定が存在しない | 新規作成 |
GET は通り PUT が 403 | メソッド未登録 | AllowedMethods |
| JSON の content type を付けた途端 403 | ヘッダー未登録 | AllowedHeaders |
アップロードは成功するが etag が null | ヘッダーが公開されていない | ExposeHeaders |
example.com は通り www.example.com が落ちる | オリジンは完全一致 | AllowedOrigins |
| 直した直後にまた失敗する | プリフライトがキャッシュ済み | MaxAgeSeconds |
S3 がルールを選ぶ順序
S3 は「uses the first CORSRule rule that matches the incoming browser request」という動作で、次の 3 つがすべて満たされたときだけ一致します。
- リクエストの
OriginヘッダーがAllowedOriginsのいずれかと一致する。 Access-Control-Request-MethodのメソッドがAllowedMethodsのいずれかと一致する。Access-Control-Request-Headersに並ぶすべてのヘッダーがAllowedHeadersのいずれかと一致する。
先に一致したものが勝つため、広いルールを狭いルールの上に置くと後者は使われません。ルールは 1 設定 100 件までで、コンソールでは「the CORS configuration must be JSON」です。XML は API と SDK では今も通ります。なお ACL とバケットポリシーはそのまま効きます。CORS が決めるのはブラウザがレスポンスを読めるかどうかで、権限の話ではありません。
5 つの要素とワイルドカードの算数
AllowedMethods に指定できるのは 5 つだけです。GET、PUT、POST、DELETE、HEAD。OPTIONS は入りません。プリフライトは許可するものではなく S3 が応答するものだからです。
設定が静かに壊れるのはワイルドカードです。AllowedOrigins の各項目は「can contain only one * wildcard character, such as http://*.example.com」、AllowedHeaders の各文字列も「can contain at most one * wildcard character」で、公式例は x-amz-*(Amazon 固有ヘッダーをすべて許可)。トラブルシューティングのページには AllowedMethods の * は全メソッドに一致するとも書かれており、5 つの一覧だけでは気づきません。
ブラウザからのアップロードと読み取りを両方カバーする設定はこうなります。
json
[
{
"AllowedOrigins": ["https://app.example.com", "http://localhost:5173"],
"AllowedMethods": ["GET", "HEAD", "PUT", "POST"],
"AllowedHeaders": ["Content-Type", "x-amz-*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]MaxAgeSeconds はブラウザがプリフライトの答えを「as identified by the resource, the HTTP method, and the origin」でキャッシュできる秒数です。大きくしておくと、直してデプロイしても効いていないように見えます。調査中は 0 にして、終わってから戻すのが早いです。
忘れられるヘッダー: ETag
最も多い「半分動く」CORS 設定です。アップロードは 200 を返し、オブジェクトはバケットにあるのに、コードが必要な値を読めません。AWS は明言しています。「if you want to read the ETag header from a PUT or multipart upload, you need to include the ExposeHeader tag in your configuration」、そして「The SDK can only access headers that are exposed through CORS configuration.」
ブラウザ側のマルチパートアップロードはこれに依存します。完了処理で各パートの ETag を送り返す必要があるため、"ExposeHeaders": ["ETag"] がなければパートは上がり完了呼び出しだけが落ちます。x-amz-meta-* として返るカスタムメタデータも同様に列挙が必要です。
localhost、プレビュー URL、資格情報つきリクエスト
AllowedOrigins の項目はワイルドカード 1 個を除けば文字列として比較されます。http://localhost:3000 と http://localhost:5173 は別のオリジン、http://localhost と https://localhost もまた別で、末尾にスラッシュを付けると何にも一致しません。プレビュー環境は「昨日までは動いていた」の定番の原因で、デプロイごとにホスト名が変わるため https://*.example.dev のようなワイルドカード 1 行で解決します。
ブラウザ側の制約が 2 つ。資格情報つきリクエストに対する MDN の規則は絶対で、サーバーは「must not specify the * wildcard for the Access-Control-Allow-Origin response-header value, but must instead specify an explicit origin」。またプリフライトを免れるのは単純リクエスト、つまり GET、HEAD、POST かつセーフリスト済みヘッダー(Accept、Accept-Language、Content-Language、MIME タイプ 3 種に限った Content-Type、Range)だけです。
curl でプリフライトを確かめる
AWS がコマンドを公開しているので推測は不要です。
sh
curl -v -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: content-type" \
"https://my-bucket.s3.eu-west-1.amazonaws.com/key.png"正しい設定なら 200 OK と Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers、そして Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method が返ります。同じページの警告が多くの混乱を説明します。「When sending a preflight request, if any of the CORS request headers are not allowed, none of the response CORS headers are returned.」何も返ってこないように見えるのは、設定が丸ごと無いのではなく、リクエストの一項目が拒否されたということです。
設定を反映させる
コンソールでは General purpose buckets → 対象バケット → Permissions → Cross-origin resource sharing (CORS) の Edit で JSON を貼り、Save changes。
リポジトリに残せるのは CLI 版です。
sh
aws s3api put-bucket-cors --bucket my-bucket --cors-configuration file://cors.json
aws s3api get-bucket-cors --bucket my-bucket必要な権限は s3:PutBucketCORS で、バケット所有者は既定で保持しています。前段に CDN があればそちらも設定します。OPTIONS を許可し、Origin、Access-Control-Request-Headers、Access-Control-Request-Method を転送し、オリジンヘッダーをキャッシュキーに含めること。AWS は「caching proxies that don't include the origin header in their cache key may serve cached responses that don't include the appropriate CORS headers for different origins」と警告しています。
署名付き URL も例外ではない
署名付き URL が持っているのは認可であって、レスポンスを読む許可ではありません。ブラウザは変わらず CORS を検査するので、JavaScript からの署名付き PUT にも同じバケット設定が必要です。調査中に覚えておきたい期限が 2 つ。期限切れの URL は CORS 失敗に見える 403 を返します。S3 コンソールの上限は 12 時間、aws s3 presign は 7 日です。署名側は S3 署名付き URL ジェネレーターで扱っています。
CORS が原因ではない場合
CORS はブラウザを守る仕組みなので、制約を受けるのはブラウザだけです。デスクトップクライアントは自分で署名し Origin ヘッダーを送らないため、バケット設定では止められません。Web アプリからは使えないバケットが、同じ時刻にアプリからは普通に開く理由がこれです。
AnyStorage 0.2.25(macOS 11 以降、Windows 10 以降、Ubuntu 20.04 以降)は、アクセスキーの組、任意のカスタムエンドポイント URL、明示的なパススタイル切り替えで S3 互換エンドポイントに接続し、カーネルドライバではなくローカルの WebDAV サーバー経由でマウントします。無料版は接続 2 件、マウントは読み取り専用です。ファイルを動かすには使えますが、Web アプリには上の JSON が必要です。S3 クライアント(GUI)の概要と Windows 版もあります。
よくある質問
AllowedOrigins を "*" にしてよいですか。
公開素材のバケットなら、["*"] と実際に使うメソッドの組み合わせは妥当です。ただしリクエストに資格情報が付く瞬間に選択肢から外れます。ブラウザが資格情報つきレスポンスでのワイルドカードを禁じているためです。AllowedHeaders は明示のままにしてください。1 文字列にワイルドカード 1 個が上限で、たいていは x-amz-* が意図に合います。
直したのに効きません。
ほぼ常にプリフライトのキャッシュです。ブラウザはリソース、メソッド、オリジンの組で前回の答えを MaxAgeSeconds の間再利用します。curl -X OPTIONS で現在の応答を確認し、プライベートウィンドウで再試行してください。
サーバーからのアップロードにも CORS は必要ですか。
不要です。CORS はブラウザの JavaScript が出すリクエストにしか適用されません。Lambda、Node のプロセス、CI ジョブ、デスクトップクライアントは対象外です。同じ資格情報が一方では通り他方で落ちる理由がこれです。
CORS ルールは何件まで入れられますか。
100 件までで、最初に一致したルールが使われます。順序が効くので、具体的なルールを一般的なルールより上に置いてください。
アップロードは成功するのに ETag が読めません。CORS ですか。
はい、配列 1 つで直ります。該当ルールに "ExposeHeaders": ["ETag"] を追加してください。ブラウザ側のマルチパートアップロードはこれなしでは完了できず、読みたい x-amz-meta-* ヘッダーも同じように列挙が必要です。