netsrikc
netsrikc

Reputation: 21

Cannot access subpages of my NextJS static site on S3 via cloudfront when public access is blocked

I have configured cloudfront to serve my NextJS static site from an S3 buckets. I have intentionally blocked all public access to my S3 bucket, so the only way to access this site would via the cloudfront URL (I've set up Origin access control "OAC" on the cloudfront distribution, so that's how cloudfront is able to access my S3 bucket. For the cloudfront origin domain, I have added the S3 bucket URL and not the static hosting endpoint, because S3 static hosting endpoint requires S3 objects to be publicly accessible, which is what I am trying to block).

I am able to go to my website and click around using the cloudfront URL. It navigates to subpages and content shows up as expected. However, refreshing on a subpage results in the 'AccessDenied' page.

For example, this is the name of the website: https://example.com Going to https://example.com works fine, it shows the index.html like I have configured. And then, clicking a button on the website takes me to https://example.com/another-page, which also shows up just fine. However, if I refresh on https://example.com/another-page, that's when the 'AccessDenied' error shows up

Are there ways to get around it, so I can go straight to the subpages? It feels like it is possible, given that I was able to navigate to https://example.com/another-page within the app itself.

Upvotes: 2

Views: 1832

Answers (2)

Sean W
Sean W

Reputation: 6598

I use a lambda function to redirect the request in the background - e.g., /route to /route/index.html but it shows as /route to the user. The below function also supports trailing slash by default. You can remove it if you want.

In your CloudFront distro, you must add a viewer request function.

"use strict";

exports.handler = (event, _, callback) => {
  // Extract the request from the CloudFront event that is sent to Lambda@Edge
  let request = event.Records[0].cf.request;
  // Extract the URI from the request
  let oldUri = request.uri;
  // If URI is a file
  const isFile = /\/[^/]+\.[^/]+$/.test(oldUri);

  // If not a file request and does not end with / redirect to /
  // if Next.js trailing slash is enabled, add the if block
  if (!isFile && !oldUri.endsWith("/")) {
    return callback(null, {
      body: "",
      status: "301",
      statusDescription: "Moved Permanently",
      querystring: request.querystring,
      headers: {
        location: [
          {
            key: "Location",
            value: `${oldUri}/`,
          },
        ],
      },
    });
  }
  // Match any '/' that occurs at the end of a URI. Replace it with a default index
  request.uri = oldUri.replace(/\/$/, "/index.html");
  // Return to CloudFront
  return callback(null, request);
};

You may need to add the correct permissions to run the function - AWS will let you know by throwing an error.


You'll also need to Configure custom error responses (CloudFront console) to your CloudFront distro.

HTTP error code: 403
Customize error response: true
Response page path: /404 (or /404/ if trailingSlash is enabled)
HTTP Response code: 404: Not found

Ensure you DO NOT CACHE any HTML pages - only cache assets, e.g., js, images, css, videos, etc. If you cache the HTML pages, your users will get the stale page if they've recently been to your site and the cache wasn't busted.

The cache is typically busted two ways.

  • Users can manually bust the cache via a cacheless refresh or deleting the cache.
  • It expires - typically set to expire after one year

If you refrain from caching the HTML pages, your site will always show the most recent version on every request without a performance hit.

I posted an answer here on how to set cache-control headers on SPAs and not cache the HMTL via the AWS CLI


Lastly, if your site is not a subdomain, I highly recommend redirecting the apex domain/www. eg https://example.com -> https://www.example.com or https://www.example.com -> https://example.com.

Upvotes: 0

netsrikc
netsrikc

Reputation: 21

I am able to get around the issue by adding a lambda function to cloudfront. The lambda function basically adds '.html' to the request, following this tutorial https://www.youtube.com/watch?v=wAJKhz2KxJA&t=628s

The solution in the video is great for non dynamic pages. He also offered a solution for pages with dynamic IDs, but I think it could be improved.

Upvotes: 0

Related Questions