If you’re using S3’s static website hosting feature to host a React or Angular webapp, the URLs in the URL bar of a browser running your webapp are essentially pseudo URLs. There’s no HTML page at the end of any of those URLs. Client-side JavaScript in your webapp handles all URLs & displays the appropriate page. In such cases, if a user types-in or visits 1 of the sub-URLs of your webapp, maybe via a bookmark, S3 will simply return a 404.
To avoid this, you must set up redirection of all URLs to index.html, which will load your webapp, which in-turn will handle the URL. In the server world, this would be easily accomplished by a redirect rule:
Apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Nginx
rewrite ^(.+)$ /index.html last;
There are many ways to handle this redirection for a static webapp hosted on S3:
Using CloudFront
Create a CloudFront distribution with your S3 bucket as the origin, index.html as the default root object, “<your-bucket>.s3-website-us-east-1.amazonaws.com” as the origin domain name, & a custom error response to redirect everything to index.html:

Using S3 Redirection Rules
Using advanced redirection rules, you can route requests conditionally according to specific object key names, prefixes in the request, or response codes.
— Configuring Advanced Conditional Redirects — S3 Documentation
In our case, use this redirection rule:
<RoutingRules>
<RoutingRule>
<Condition>
<HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
</Condition>
<Redirect>
<HostName>yourdomainname.com</HostName>
<ReplaceKeyPrefixWith>#!/</ReplaceKeyPrefixWith>
</Redirect>
</RoutingRule>
</RoutingRules>

There are a few problems with this approach though:
- Multiple redirects happen as your app’s paths are resolved.
- There is a flicker in the url bar as the ‘#’ comes & goes due the action of your SPA framework.
- SEO is impacted.
- Safari support is broken!
Using S3 Error Document
Set the S3 error document to index.html:

In this case, even though the user gets the webapp, the status code is still 404, which isn’t good for SEO.
Using Lambda@Edge
Use the compute power at the CloudFront edge to inspect the request as it’s coming in from the client. Then re-write the request so that CloudFront requests a default index object (index.html in this case) for any request URI that ends in ‘/’.
— AWS Compute Blog
Lambda Code:
'use strict';
exports.handler = (event, context, callback) => {
// Extract the request from the CloudFront event that is sent to Lambda@Edge
var request = event.Records[0].cf.request;
// Extract the URI from the request
var olduri = request.uri;
// Match any '/' that occurs at the end of a URI. Replace it with a default index
var newuri = olduri.replace(/\/$/, '\/index.html');
// Log the URI as received by CloudFront and the new URI to be used to fetch from origin
console.log("Old URI: " + olduri);
console.log("New URI: " + newuri);
// Replace the received URI with the URI that includes the index page
request.uri = newuri;
// Return to CloudFront
return callback(null, request);
};
See full article here — AWS Compute Blog.
Using Amplify Redirects
If your webapp is built at the AWS Amplify console, use this:
Most SPA frameworks support HTML5 history.pushState() to change browser location without triggering a server request. This works for users who begin their journey from the root (or /index.html), but fails for users who navigate directly to any other page.
— Redirects for Single Page Web Apps (SPA) — Amplify Documentation

Copy the regex from here — Redirects for Single Page Web Apps (SPA).