Skip to content

Commit

Permalink
Merge pull request #165 from adriencaccia/fix/statix-website
Browse files Browse the repository at this point in the history
Fix SPA error page behavior
  • Loading branch information
mnapoli committed Feb 7, 2022
2 parents ef0936a + e5da8c9 commit 0d164da
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 68 deletions.
32 changes: 10 additions & 22 deletions src/constructs/aws/StaticWebsite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import {
} from "@aws-cdk/aws-cloudfront";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import type { Construct as CdkConstruct } from "@aws-cdk/core";
import { CfnOutput, Duration, RemovalPolicy } from "@aws-cdk/core";
import { CfnOutput, RemovalPolicy } from "@aws-cdk/core";
import type { FromSchema } from "json-schema-to-ts";
import chalk from "chalk";
import { S3Origin } from "@aws-cdk/aws-cloudfront-origins";
import * as acm from "@aws-cdk/aws-certificatemanager";
import { flatten } from "lodash";
import type { ErrorResponse } from "@aws-cdk/aws-cloudfront/lib/distribution";
import type { AwsProvider } from "@lift/providers";
import { AwsConstruct } from "@lift/constructs/abstracts";
import type { ConstructCommands } from "@lift/constructs";
Expand Down Expand Up @@ -91,6 +90,11 @@ export class StaticWebsite extends AwsConstruct {
}

const bucket = new Bucket(this, "Bucket", {
// Enable static website hosting
websiteIndexDocument: "index.html",
websiteErrorDocument: this.errorResponseDocument(),
// Required when static website hosting is enabled
publicReadAccess: true,
// For a static website, the content is code that should be versioned elsewhere
removalPolicy: RemovalPolicy.DESTROY,
});
Expand Down Expand Up @@ -120,8 +124,6 @@ export class StaticWebsite extends AwsConstruct {

this.distribution = new Distribution(this, "CDN", {
comment: `${provider.stackName} ${id} website CDN`,
// Send all page requests to index.html
defaultRootObject: "index.html",
defaultBehavior: {
// Origins are where CloudFront fetches content
origin: new S3Origin(bucket),
Expand All @@ -132,7 +134,6 @@ export class StaticWebsite extends AwsConstruct {
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
functionAssociations: functionAssociations,
},
errorResponses: [this.errorResponse()],
// Enable http2 transfer for better performances
httpVersion: HttpVersion.HTTP2,
certificate: certificate,
Expand Down Expand Up @@ -278,39 +279,26 @@ export class StaticWebsite extends AwsConstruct {
return this.provider.getStackOutput(this.distributionIdOutput);
}

private errorResponse(): ErrorResponse {
private errorResponseDocument(): string {
// Custom error page
if (this.configuration.errorPage !== undefined) {
let errorPath = this.configuration.errorPage;
const errorPath = this.configuration.errorPage;
if (errorPath.startsWith("./") || errorPath.startsWith("../")) {
throw new ServerlessError(
`The 'errorPage' option of the '${this.id}' static website cannot start with './' or '../'. ` +
`(it cannot be a relative path).`,
"LIFT_INVALID_CONSTRUCT_CONFIGURATION"
);
}
if (!errorPath.startsWith("/")) {
errorPath = `/${errorPath}`;
}

return {
httpStatus: 404,
ttl: Duration.seconds(0),
responseHttpStatus: 404,
responsePagePath: errorPath,
};
return errorPath;
}

/**
* The default behavior is optimized for SPA: all unknown URLs are served
* by index.html so that routing can be done client-side.
*/
return {
httpStatus: 404,
ttl: Duration.seconds(0),
responseHttpStatus: 200,
responsePagePath: "/index.html",
};
return "index.html";
}

private createResponseFunction(): cloudfront.Function {
Expand Down
68 changes: 22 additions & 46 deletions test/unit/staticWebsites.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ describe("static websites", () => {
});
const bucketLogicalId = computeLogicalId("landing", "Bucket");
const bucketPolicyLogicalId = computeLogicalId("landing", "Bucket", "Policy");
const originAccessIdentityLogicalId = computeLogicalId("landing", "CDN", "Origin1", "S3Origin");
const responseFunction = computeLogicalId("landing", "ResponseFunction");
const cfDistributionLogicalId = computeLogicalId("landing", "CDN");
const cfOriginId = computeLogicalId("landing", "CDN", "Origin1");
Expand All @@ -36,13 +35,18 @@ describe("static websites", () => {
bucketLogicalId,
bucketPolicyLogicalId,
responseFunction,
originAccessIdentityLogicalId,
cfDistributionLogicalId,
]);
expect(cfTemplate.Resources[bucketLogicalId]).toMatchObject({
Type: "AWS::S3::Bucket",
UpdateReplacePolicy: "Delete",
DeletionPolicy: "Delete",
Properties: {
WebsiteConfiguration: {
ErrorDocument: "index.html",
IndexDocument: "index.html",
},
},
});
expect(cfTemplate.Resources[bucketPolicyLogicalId]).toMatchObject({
Properties: {
Expand All @@ -55,9 +59,7 @@ describe("static websites", () => {
Action: "s3:GetObject",
Effect: "Allow",
Principal: {
CanonicalUser: {
"Fn::GetAtt": [originAccessIdentityLogicalId, "S3CanonicalUserId"],
},
AWS: "*",
},
Resource: { "Fn::Join": ["", [{ "Fn::GetAtt": [bucketLogicalId, "Arn"] }, "/*"]] },
},
Expand All @@ -66,26 +68,10 @@ describe("static websites", () => {
},
},
});
expect(cfTemplate.Resources[originAccessIdentityLogicalId]).toMatchObject({
Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity",
Properties: {
CloudFrontOriginAccessIdentityConfig: {
Comment: `Identity for ${cfOriginId}`,
},
},
});
expect(cfTemplate.Resources[cfDistributionLogicalId]).toMatchObject({
Type: "AWS::CloudFront::Distribution",
Properties: {
DistributionConfig: {
CustomErrorResponses: [
{
ErrorCachingMinTTL: 0,
ErrorCode: 404,
ResponseCode: 200,
ResponsePagePath: "/index.html",
},
],
DefaultCacheBehavior: {
AllowedMethods: ["GET", "HEAD", "OPTIONS"],
Compress: true,
Expand All @@ -100,29 +86,25 @@ describe("static websites", () => {
},
],
},
DefaultRootObject: "index.html",
Enabled: true,
HttpVersion: "http2",
IPV6Enabled: true,
Origins: [
{
DomainName: {
"Fn::GetAtt": [bucketLogicalId, "RegionalDomainName"],
},
Id: cfOriginId,
S3OriginConfig: {
OriginAccessIdentity: {
"Fn::Join": [
"",
[
"origin-access-identity/cloudfront/",
"Fn::Select": [
2,
{
"Fn::Split": [
"/",
{
Ref: originAccessIdentityLogicalId,
"Fn::GetAtt": [bucketLogicalId, "WebsiteURL"],
},
],
],
},
},
],
},
Id: cfOriginId,
},
],
},
Expand Down Expand Up @@ -369,19 +351,13 @@ describe("static websites", () => {
},
}),
});
const cfDistributionLogicalId = computeLogicalId("landing", "CDN");
expect(cfTemplate.Resources[cfDistributionLogicalId]).toMatchObject({

const bucketLogicalId = computeLogicalId("landing", "Bucket");

expect(cfTemplate.Resources[bucketLogicalId]).toMatchObject({
Properties: {
DistributionConfig: {
CustomErrorResponses: [
{
// The response code is forced to 404 and changed to /error.html
ErrorCachingMinTTL: 0,
ErrorCode: 404,
ResponseCode: 404,
ResponsePagePath: "/my/custom/error.html",
},
],
WebsiteConfiguration: {
ErrorDocument: "my/custom/error.html",
},
},
});
Expand Down

0 comments on commit 0d164da

Please sign in to comment.