Search code examples
amazon-web-servicesaws-lambdaamazon-cloudfrontaws-cdkaws-rest-api

AWS Cloudfront for S3 backed website + Rest API: (Error - MethodNotAllowed / The specified method is not allowed against this resource)


I have an AWS S3 backed static website and a RestApi. I am configuring a single Cloudfront Distribution for the static website and the RestApi. I have OriginConfigs done for the S3 origins and the RestApi origin. I am using AWS CDK to define the infrastructure in code.

The approach has been adopted from this article: https://dev.to/evnz/single-cloudfront-distribution-for-s3-web-app-and-api-gateway-15c3]

The API are defined under the relative path /r/<resourcename> or /r/api/<methodname>. Examples would be /r/Account referring to the Account resource and /r/api/Validate referring to an rpc-style method called Validate (in this case a HTTP POST method). The Lambda methods that implement the resource methods are configured with the proper PREFLIGHT OPTIONS with the static website's url listed in the allowed origins for that resource. For eg: the /r/api/Validate method lambda has

exports.main = async function(event, context) {
  try {
    var method = event.httpMethod;

    if(method === "OPTIONS") {
      const response = {
        statusCode: 200,
        headers: {
          "Access-Control-Allow-Headers" : "*",
          "Access-Control-Allow-Credentials": true,
          "Access-Control-Allow-Origin": website_url,
          "Vary": "Origin",
          "Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE"
        }
      };
      return response;
    } else if(method === "POST") {
      ...
    }
   ...
}

The API and website are deployed fine. Here's the CDK deployment code fragment.

        const string api_domain = "myrestapi.execute-api.ap-south-1.amazonaws.com";
        const string api_stage = "prod";

        internal WebAppStaticWebsiteStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
        {
            // The S3 bucket to hold the static website contents
            var bucket = new Bucket(this, "WebAppStaticWebsiteBucket", new BucketProps {
                PublicReadAccess = false,
                BlockPublicAccess = BlockPublicAccess.BLOCK_ALL,
                RemovalPolicy = RemovalPolicy.DESTROY,
                WebsiteIndexDocument = "index.html",
                Cors = new ICorsRule[] {
                    new CorsRule() {
                        AllowedHeaders = new string[] { "*" },
                        AllowedMethods = new HttpMethods[] { HttpMethods.GET, HttpMethods.POST, HttpMethods.PUT, HttpMethods.DELETE, HttpMethods.HEAD },
                        AllowedOrigins = new string[] { "*" }
                    }
                }
            });

            var cloudfrontOAI = new OriginAccessIdentity(this, "CloudfrontOAI", new OriginAccessIdentityProps() {
                Comment = "Allows cloudfront access to S3"
            });

            bucket.AddToResourcePolicy(new PolicyStatement(new PolicyStatementProps() {
                Sid = "Grant cloudfront origin access identity access to s3 bucket",
                Actions = new [] { "s3:GetObject" },
                Resources = new [] { bucket.BucketArn + "/*" },
                Principals = new [] { cloudfrontOAI.GrantPrincipal }
            }));

            // The cloudfront distribution for the website
            var distribution = new CloudFrontWebDistribution(this, "WebAppStaticWebsiteDistribution", new CloudFrontWebDistributionProps() {
                ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                DefaultRootObject = "index.html",
                PriceClass = PriceClass.PRICE_CLASS_ALL,
                GeoRestriction = GeoRestriction.Whitelist(new [] {
                    "IN"
                }),
                OriginConfigs = new [] {
                    new SourceConfiguration() {
                        CustomOriginSource = new CustomOriginConfig() {
                            OriginProtocolPolicy = OriginProtocolPolicy.HTTPS_ONLY,
                            DomainName = api_domain,
                            AllowedOriginSSLVersions = new OriginSslPolicy[] { OriginSslPolicy.TLS_V1_2 },
                        },
                        Behaviors = new IBehavior[] {
                            new Behavior() {
                                IsDefaultBehavior = false,
                                PathPattern = $"/{api_stage}/r/*",
                                AllowedMethods = CloudFrontAllowedMethods.ALL,
                                CachedMethods = CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
                                DefaultTtl = Duration.Seconds(0),
                                ForwardedValues = new CfnDistribution.ForwardedValuesProperty() {
                                    QueryString = true,
                                    Headers = new string[] { "Authorization" }
                                }
                            }
                        }
                    },
                    new SourceConfiguration() {
                        S3OriginSource = new S3OriginConfig() {
                            S3BucketSource = bucket,
                            OriginAccessIdentity = cloudfrontOAI
                        },
                        Behaviors = new [] {
                            new Behavior() {
                                IsDefaultBehavior = true,
                                //PathPattern = "/*",
                                DefaultTtl = Duration.Seconds(0),
                                Compress = false,
                                AllowedMethods = CloudFrontAllowedMethods.ALL,
                                CachedMethods = CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS
                            }
                        },
                    }
                }
            });

            // The distribution domain name - output
            var domainNameOutput = new CfnOutput(this, "WebAppStaticWebsiteDistributionDomainName", new CfnOutputProps() {
                Value = distribution.DistributionDomainName
            });

            // The S3 bucket deployment for the website
            var deployment = new BucketDeployment(this, "WebAppStaticWebsiteDeployment", new BucketDeploymentProps(){
                Sources = new [] {Source.Asset("./website/dist")},
                DestinationBucket = bucket,
                Distribution = distribution
            });
        }

I am encountering the following error (extracted from Browser console error log):

bundle.js:67 POST https://mywebapp.cloudfront.net/r/api/Validate 405

bundle.js:67 
<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>MethodNotAllowed</Code>
  <Message>The specified method is not allowed against this resource.</Message>
  <Method>POST</Method>
  <ResourceType>OBJECT</ResourceType>
  <RequestId>xxxxx</RequestId>
  <HostId>xxxxxxxxxxxxxxx</HostId>
</Error>

The intended flow is that the POST call (made using fetch() api) to https://mywebapp.cloudfront.net/r/api/Validate is forwarded to the RestApi backend by cloudfront. It appears like Cloudfront is doing it, but the backend is returning an error (based on the error message).

What am I missing? How do I make this work?


Solution

  • This was fixed by doing the following:

    1. Moved to the Distribution construct (which as per AWS documentation is the one to use as it is receiving latest updates).
    2. Adding a CachePolicy and OriginRequestPolicy to control Cookie forwarding and Header forwarding