FIX: Missing CORS
Deploy Infrastructure / pulumi-up (push) Successful in 2s

This commit is contained in:
FahdShalhoub
2026-08-29 18:51:52 +03:00
parent c3465bedb1
commit f66f3b555d
2 changed files with 73 additions and 5 deletions
+8 -1
View File
@@ -372,7 +372,14 @@ on boot if any required var is empty.
- **S3 + CloudFront**: `encoded-bucket` holds finished transcoded output,
served publicly via CloudFront using Origin Access Control (OAC) — the
bucket itself blocks all public access; only CloudFront's OAC principal
can read it.
can read it. The distribution carries a **response headers policy** adding
permissive CORS headers (`*`, `GET`/`HEAD`/`OPTIONS`), and `OPTIONS` is in
the behaviour's allowed/cached methods so CloudFront answers preflights
itself: HLS is fetched by JavaScript, so a playlist or segment served
without `Access-Control-Allow-Origin` is discarded by the browser. The
bucket also carries its own equivalent CORS rule, which is what the
LocalStack stack relies on — there is no CloudFront there and
`PLAYBACK_BASE_URL` points the player straight at S3.
- **S3 raw uploads**: `raw-uploads-bucket` is a separate, private bucket for
pre-transcode uploads — deliberately kept apart from `encoded-bucket` so
raw source video is never reachable through the public CDN. CORS is
+63 -2
View File
@@ -426,6 +426,42 @@ func main() {
return err
}
// HLS is played by JavaScript (hls.js fetches the .m3u8 playlist
// and every .ts/.m4s segment with XHR), so the CDN has to answer
// with CORS headers or the browser drops the response — the
// "CORS Missing Allow Origin" failure. The S3 origin sends none of
// its own, so CloudFront adds them here, at the edge, for cached
// and uncached responses alike.
corsHeaders, err := cloudfront.NewResponseHeadersPolicy(ctx, "encoded-bucket-cors-headers", &cloudfront.ResponseHeadersPolicyArgs{
Comment: pulumi.String("CORS headers so browsers can fetch HLS playlists and segments"),
CorsConfig: &cloudfront.ResponseHeadersPolicyCorsConfigArgs{
AccessControlAllowCredentials: pulumi.Bool(false),
AccessControlAllowHeaders: &cloudfront.ResponseHeadersPolicyCorsConfigAccessControlAllowHeadersArgs{
Items: pulumi.ToStringArray([]string{"*"}),
},
AccessControlAllowMethods: &cloudfront.ResponseHeadersPolicyCorsConfigAccessControlAllowMethodsArgs{
Items: pulumi.ToStringArray([]string{"GET", "HEAD", "OPTIONS"}),
},
// Playback is public and unauthenticated, so any origin may
// read it. A wildcard also keeps Origin out of the cache
// key: with a fixed list CloudFront would have to vary the
// cached response on the request's Origin header.
AccessControlAllowOrigins: &cloudfront.ResponseHeadersPolicyCorsConfigAccessControlAllowOriginsArgs{
Items: pulumi.ToStringArray([]string{"*"}),
},
// Content-Range/Content-Length are what a player reads back
// when it seeks with a Range request.
AccessControlExposeHeaders: &cloudfront.ResponseHeadersPolicyCorsConfigAccessControlExposeHeadersArgs{
Items: pulumi.ToStringArray([]string{"Content-Length", "Content-Range", "Date", "ETag"}),
},
AccessControlMaxAgeSec: pulumi.Int(3000),
OriginOverride: pulumi.Bool(true),
},
})
if err != nil {
return err
}
distribution, err = cloudfront.NewDistribution(ctx, "encoded-bucket-cdn", &cloudfront.DistributionArgs{
Enabled: pulumi.Bool(true),
Comment: pulumi.String("Edge caching for encoded-bucket assets"),
@@ -442,10 +478,14 @@ func main() {
DefaultCacheBehavior: &cloudfront.DistributionDefaultCacheBehaviorArgs{
TargetOriginId: pulumi.String(originId),
ViewerProtocolPolicy: pulumi.String("redirect-to-https"),
AllowedMethods: pulumi.ToStringArray([]string{"GET", "HEAD"}),
CachedMethods: pulumi.ToStringArray([]string{"GET", "HEAD"}),
// OPTIONS is listed so CloudFront answers CORS preflights
// itself from the response headers policy below; without it
// a preflight is rejected with 403 before the policy runs.
AllowedMethods: pulumi.ToStringArray([]string{"GET", "HEAD", "OPTIONS"}),
CachedMethods: pulumi.ToStringArray([]string{"GET", "HEAD", "OPTIONS"}),
Compress: pulumi.Bool(true),
CachePolicyId: pulumi.String(cachingOptimizedPolicyId),
ResponseHeadersPolicyId: corsHeaders.ID(),
},
Restrictions: &cloudfront.DistributionRestrictionsArgs{
GeoRestriction: &cloudfront.DistributionRestrictionsGeoRestrictionArgs{
@@ -1274,6 +1314,27 @@ func main() {
corsOrigins = pulumi.StringArray{pulumi.Sprintf("http://%s", cmsAlb.DnsName)}
}
// The encoded bucket needs its own CORS rule for the LocalStack stack,
// where there is no CloudFront in front of it and PLAYBACK_BASE_URL
// points the player straight at S3. Behind CloudFront the response
// headers policy already covers playback, but the rule is harmless
// there and keeps a direct-to-bucket player working either way.
_, err = s3.NewBucketCorsConfigurationV2(ctx, "encoded-bucket-cors", &s3.BucketCorsConfigurationV2Args{
Bucket: bucket.ID(),
CorsRules: s3.BucketCorsConfigurationV2CorsRuleArray{
&s3.BucketCorsConfigurationV2CorsRuleArgs{
AllowedMethods: pulumi.ToStringArray([]string{"GET", "HEAD"}),
AllowedOrigins: pulumi.ToStringArray([]string{"*"}),
AllowedHeaders: pulumi.ToStringArray([]string{"*"}),
ExposeHeaders: pulumi.ToStringArray([]string{"Content-Length", "Content-Range", "Date", "ETag"}),
MaxAgeSeconds: pulumi.Int(3000),
},
},
})
if err != nil {
return err
}
_, err = s3.NewBucketCorsConfigurationV2(ctx, "raw-uploads-bucket-cors", &s3.BucketCorsConfigurationV2Args{
Bucket: rawUploadsBucket.ID(),
CorsRules: s3.BucketCorsConfigurationV2CorsRuleArray{