score:104

Accepted answer

Update 1 May 2020:

Since this post is quite active, I need to update the answer:

So you have a few options to solve the issue:

  1. You can put index.html in the Error document box (like Alan Friedman suggested).
    • Go to your bucket (the one that actually has the code - not the one you use to redirect) -> Properties -> Static website hosting:
    • This is not "hacky" but it works because of the way react-router works: It handles the requests from the front-end and routes users to other routes but overall, the React app is a single-page application.
    • If you want server-side React, consider using Next.js.

enter image description here

  1. You can put a designated error file error.html in the public folder of your React app and in the Static website hosting: Error document box, put in: error.html. This also works. I've tested it.

  2. Use AWS CloudFront > CloudFront Distributions > The distribution of your bucket > Error Pages and add the error code you want. If you don't use react-router (like the example below), the bucket will respond with Error 403 so you can respond with your error.html.

enter image description here

  1. For TypeScript, currently react-router doesn't support it so you should consider option 2 and 3. They're going to update the library in future version 6.* according to this thread.

score:-1

As per previous answers, the best way to solve the problem: redefine the fallback strategy. If you'd like to learn more about client-side routing vs server-side, and particularly about different routing approaches in the React JS, check out this article.

score:0

I fixed it by using HashRouter instead of Router

import { Switch, Route, HashRouter } from 'react-router-dom';

const App = () => (
    <HashRouter>
        <div>
            <NavBar/>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route exact path="/projects" component={Projects}/>
                <Route exact path="/about" component={About}/>
                <Route component={NoMatch}/>
            </Switch>
        </div>
    </HashRouter>
);

The app is acceccible by something like https://your-domain.s3.amazonaws.com/index.html?someParam=foo&otherPram=bar/#/projects

on localhost like https://localhost:3000/?someParam=foo&otherPram=bar/#/projects

No changes in S3 were needed.

score:0

I'm using NextJS and had the same issue. My solution was to check the routing information and push if it doesn't fit.

const router = useRouter();
useEffect(() => {
    if (!router.pathname.startsWith(router.asPath)) {
      router.push(router.asPath);
    }
  });

Nothing needed to be modified at the S3 side except from routing the error page to index.html

score:1

Why it happens

Your issue is that you want to pass responsibility to routing to your react app/javascript. The link will work because react can listen to the link click and simply update the route in the browser URL bar. However, if you go a location where your script (index.html and the bundle.js or wherever your app code is located) is not loaded, then the JavaScript is never loaded and has no chance to handle the request. Instead, whatever runs your server will take care of the request, look if there is a resource at this place, and return a 404 error or whatever else it found.

The solution

As mentioned in the comments, this is the reason why you need to redirect 404-errors to the exact location where your app is placed. This is nothing specific to Amazon, it is a general requirement to set up your react app.

To solve the issue you need to find out what handles routing on your server and how to configure it. For example if you have an Apache server, an .htaccess file could take care of the matter like this:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.html [L]
 </IfModule>

This file lets the server redirect not found errors to the index.html. Keep in mind that it might affect other routing rules that are on your server, so this configuration is easiest, if your react app has a place on its own, not interfering with other stuff.

score:1

I ran into this issue despite using the configuration as described in the accepted answer, but if you still get 403 errors, AND you're using AWS Cloudflare Manager, you can create an error page in the "Error Pages" tab of the distribution settings, with the following configuration:

Error Code: 404,
Customize Error Response: Yes, 
Response Page Path: /index.html, 
HTTP Response Code: 200

Pic of Error Page Config to pass route handling to browser router

This worked for me although I'm by no means knowledgeable about proper server admin, hopefully someone will tell me if this is a major issue, if serendipitous.

score:1

update config of s3 with hosting static and default with index.html enter image description here

add CloudFront with error page

404 error-> index.html->status 200

enter image description here

score:1

100% Working and Tested to host SPA website like React in S3 Bucket and fix 404 sub pages issue

  1. Login to amazon console
  2. search for Cloud front
  3. Select your Distributions
  4. Select Error Pages tab and Click on Create custom Error Response enter image description here
  5. Create a response rule here enter image description here
  6. Save error rule

You have Done!

score:1

There are many answers here so I thought I would do a survey of the existing ones to date, and compare pros and cons of the various approaches.

Approach Description Pros Cons Answers
CloudFront Function Use an AWS CloudFront function to write requests to the root document of the SPA e.g. index.html No error thrown, no redirection, apparently good for SEO. AWS suggest this approach in one of their docs. Extra cost (although at the time of writing this the free tier allows 2M invocations free per month and after that it's only 10c per million invocations). Extra infrastructure to deploy/manage. KG
Host as website Configure the S3 bucket to host a website, rather than a REST API. Routing handled automatically by S3 when it's configured this way. Can't use Orign Access Identity. Rather, the S3 endpoint is exposed over HTTP and you need to restrict access using custom headers, which is more complicated. FV
Error Redirection (404 and 403) The vast majority of answers seem to offer some flavour of this. The idea is to allow the 404 or 403 error to happen, but then return index.html (or whatever the SPA root document is) as the error page. Easy to implement. Causes a redirect for the client every time they try to access a non-root URL. Apparently this can be bad for SEO. AR, V, MG, PB, K, KS
URL Hashes / Hashbangs Some solutions involve routing sub-pages using hashes in the URLs. No redirection & no cost for extra AWS resources. There seem to be issues with these approaches - see here. Hashes in URLs are considered ugly by some; there do seem to be ways to mitigate this, but that involves some extra complexity. AA, B

Note: I am basing this answer on what I've read in other answers. I have not fully tested any approach except for the "CloudFront Function" approach, which I can confirm works.

score:2

This thread has been really helpful for me. I know this is old, but, I'd like to add a aws-cdk code snippet that finally worked for me. My use case needed me to utilize aws-cdk, so, I'm adding it here if you're looking forward to a aws-cdk based solution.

The code snippet adds index.html as error page in s3 and also in cloudfront. For cloudfront specifically, I added the error page as /index.html for 404 and 403 with a response of 200


// imports...
// import * as cdk from '@aws-cdk/core';
// import * as s3 from '@aws-cdk/aws-s3';
// import * as cloudfront from '@aws-cdk/aws-cloudfront';

// following code snippet has to be added in a 
// class which extends to a Construct or a Stack

    const bucket = new s3.Bucket(this, '<Specify a name>', {
      bucketName: '<add your bucket name>',
      websiteIndexDocument: 'index.html',
      websiteErrorDocument: 'index.html',
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // edit it according to your use case, I'll change it though
      autoDeleteObjects: true,
    });

    const cloudFrontOAI = new cloudfront.OriginAccessIdentity(this, 'OAI');

    const distribution = new cloudfront.CloudFrontWebDistribution(this, props?.stackName + 'AbcWebsiteDistribution', {
      defaultRootObject: "index.html",
      errorConfigurations: [{
        errorCode: 404,
        responsePagePath: "/index.html",
        responseCode: 200
      }, {
        errorCode: 403,
        responsePagePath: "/index.html",
        responseCode: 200
      }],
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: bucket,
            originAccessIdentity: cloudFrontOAI,
            originPath: "/artifacts" // change this based on your use case
          },
          behaviors: [{ isDefaultBehavior: true }]
        }
      ]
    })

    bucket.grantRead(cloudFrontOAI.grantPrincipal);

Answers that really helped me from this thread are:

https://stackoverflow.com/a/56655629/6211961

https://stackoverflow.com/a/58978355/6211961

https://stackoverflow.com/a/64201582/6211961

score:4

There are some good answers in this thread mostly around configuring your S3 bucket with redirection rules. Although this would have also worked for my use case, which is typically hosting a React single page application using React Router through CloudFront with S3 configured as the Origin, I decided to try a different approach given a kind of new feature which was recently released, CloudFront Functions. The main idea is to rewrite the URL before it reaches CloudFront cache so that URLs created by React Router on the client side are rewritten to point to the application domain root.

You can read more about CloudFront Functions here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html

The main idea is to use a CloudFront Function for rewriting the request Uri for any requests that are not targeting static files, i.e .js, .CSS, .html etc... and making it point to the /index.html file. This function would run as part of the viewer request trigger and run before CloudFront checks to see whether the requested object is in the CloudFront cache. That would result in any Uri sent by the client that is generated by the React Router to be rewritten before the content is requested from the origin server, thus not requiring to do any further redirects on S3. At the same time, files that are requested but do not exist anymore on the origin would return a 404 as expected, while all other static content would be served as requested.

Here is how my CloudFront function looks like:

function handler(event) {
  var request = event.request;

  if (!request.uri.includes('.')) {
    request.uri = "/index.html";
    request.querystring = {}
  }

  return request;
}

You can make the URL re-writing rules as complex as you like, but surprisingly, this small piece of code works fine for my use case. Also, unlike Lambda@Edge functions, CloudFront Functions are much more lightweight and allow you to experiment almost real time through AWS console by changing the code of your function there, testing it and deploying it within a couple seconds. However, they come with certain language limitations, so do not expect all JavaScript language features to be supported. This page documents what is available for you to use:

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html

score:8

This question already has several great answers. Although the question is now old, I faced this problem today, so I feel that perhaps this answer can help.

S3 has two kinds of end points, and if you are facing this error, you have probably selected the S3 bucket directly as the endpoint in the Origin Domain Name field for your CloudFront Distribution.

This would look something like: bucket-name.s3.amazonaws.com and is a perfectly valid endpoint. In fact, it would seem that AWS does expect this as default behaviour; this entry will be in the drop-down list you have when you are creating your CloudFront distribution.

However, doing this and then setting error pages may or may not solve your problem.

S3 has, however, a dedicated Website Endpoint. This is accessible from your S3 Bucket > Properties > Static Website Hosting. (You will see the link on the top).

You should use this link instead of the original auto-populated link that comes up in CloudFront. You can put this in while creating your distribution, or after creation, you can edit from the tab Origins and Origins Groups, and then invalidating caches.

This link will look like: bucket-name.s3-website.region-name.amazonaws.com.

Once this propagates and changes, this should then fix your problem.

tl;dr: Don't use the default S3 endpoint in CloudFront. Use the S3 website endpoint instead. Your origin domain should look like this: my-application.s3-website.us-east-1.amazonaws.com and not like my-application.s3.amazonaws.com.

More Information about website endpoints is available in the AWS Documentation here.

score:8

Below configuration resolved enter image description here

Updated error pages in associated cloud-front distribution with custom error response from http status code 404-not-found to code 200-OK at response page path as / and near zero time-to-live

score:9

Case use Cloudfront to S3:

https://hackernoon.com/hosting-static-react-websites-on-aws-s3-cloudfront-with-ssl-924e5c134455

3b) AWS CloudFront — Error Pages After creating the CloudFront distribution, while its status is In Progress, proceed to the Error Pages tab. Handle response codes 404 and 403 with Customize Error Response.

Google recommends 1 week or 604800 seconds of caching. What we are doing here is to set up CloudFront to handle missing html pages, which typically occurs when a user enters an invalid path or, in particular, when they refresh a path other than the root path.

When that happens:

CloudFront will be looking for a file that does not exist in the S3 bucket; there is only 1 html file in the bucket and that is the index.html for the case of a Single Page Application like this project example A 404 response will be returned and our custom error response setup will hijack it. We will return a 200 response code and the index.html page instead. React router, which will be loaded along with the index.html file, will look at the url and render the correct page instead of the root path. This page will be cache for the duration of the TTL for all requests to the queried path. Why do we need to handle 403 as well? It is because this response code, instead of 404, is returned by Amazon S3 for assets that are not present. For instance, a url of https://yourdomain.com/somewhere will be looking for a file called somewhere (without extension) that does not exist.

PS. It used to be returning 404, but it seems to be returning 403 now; either way it is best to handle both response codes).

score:26

Redirecting 4xx to index.html would work, but that solution would make you fall into many other problems like google won't be able to crawl these pages. Please check this link, it solved this issue by using redirection rules for the S3 bucket.

score:38

In case this S3 bucket is served from a CloudFront distribution:

Create a custom error page (AWS Docs) in CloudFront distribution that routes 404 errors to index.html and returns 200 response code. This way your application will handle the routing.

Custom Error Pages

enter image description here

score:172

I'm not sure if you have already solved it. I had the same issue.

It's because AWS S3 is looking for that folder (projects) to serve from, which you don't have.

Simply point Error document to index.html.

enter image description here

This worked for me. You can handle error pages in react-router.


Related Query

More Query from same tag