Skip to content

fix(aws/router): build the S3 key from the baseless uri - #6980

Open
OmeshKandregula wants to merge 2 commits into
anomalyco:devfrom
OmeshKandregula:fix/router-base-path-s3-key
Open

fix(aws/router): build the S3 key from the baseless uri#6980
OmeshKandregula wants to merge 2 commits into
anomalyco:devfrom
OmeshKandregula:fix/router-base-path-s3-key

Conversation

@OmeshKandregula

Copy link
Copy Markdown

The problem

A site deployed with sst.aws.Nextjs whose next.config.ts sets a basePath
serves its HTML fine and then fails to load a single stylesheet or script. The
page renders as unstyled markup. Every request under <base>/_next/static/*
comes back as:

HTTP/1.1 403 Forbidden
Server: AmazonS3
Via: 1.1 ...cloudfront.net (CloudFront)

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>

The 403 is misleading and cost me a while: it is a missing key, not a
permissions problem. The OAC principal has s3:GetObject but not
s3:ListBucket, so S3 answers 403 rather than 404.

Cause

Both S3 branches of the router function match against the baseless uri and
then build the S3 key from the full one, so the base path ends up in the key.
The assets are uploaded without it.

platform/src/components/aws/router.ts, inside CF_ROUTER_INJECTION:

const baselessUri = metadata.base
  ? event.request.uri.replace(metadata.base, "")
  : event.request.uri;

// Route to S3 files
try {
  // check using baselessUri b/c files are stored in the root      <-- intent
  const u = decodeURIComponent(baselessUri);
  ...
  event.request.uri = metadata.s3.dir + event.request.uri + v;     <-- full uri
// Route to S3 routes
    if (baselessUri.startsWith(route)) {                           // baseless
      event.request.uri = metadata.s3.dir + event.request.uri;     // full uri

The comment on the first branch already states the intent: files are stored in
the root. sst.aws.Nextjs uploads them accordingly, with no base awareness:

// platform/src/components/aws/nextjs.ts
assets: [
  { from: ".open-next/assets", to: "_assets", cached: true, versionedSubDir: "_next", ... }
],

So for a site based at /admin/board, CloudFront asks S3 for

/_assets/admin/board/_next/static/css/<hash>.css

while the deploy uploaded

/_assets/_next/static/css/<hash>.css

The change

Two lines: build the key from baselessUri in both branches, which is what the
existing comment says should happen.

Tests

Seven cases in platform/test/components/cloudfront.test.ts. Four of them fail
on the parent commit with the base left in the key:

expected '/_assets/admin/board/_next/static/css…' to be '/_assets/_next/static/css/abc123.css'
expected '/_assets/admin/board/_next/static/chu…' not to contain '/admin/board'
expected '/_assets/admin/board/about.html'       to be '/_assets/about.html'
expected '/_assets/admin/board/docs/guide/index…' to be '/_assets/docs/guide/index.html'

The other three pass either way on purpose. They are guards rather than
reproductions, so that a later change to the key building cannot quietly break
what already worked: a site with no base at all, the S3 origin actually being
selected, and a path the S3 routes do not match still reaching the server.

loadRouteSite now takes the same input type as createContext. It was
declaring a narrower one, so a test could not stub the KV store or observe
which origin was chosen, and the KVS-matched case needs both.

Two things I did not touch

  • The custom404 branch goes the other way and adds the base explicitly
    (metadata.s3.dir + (metadata.base ? metadata.base : "") + metadata.custom404).
    If files really are stored in the root then that looks wrong too, but I have
    not exercised that path and did not want to change behaviour I could not
    test.
  • The "route unmatched to S3" branch has the same shape. It only runs for a
    site with no servers, where it exists to trigger customErrorResponses, so I
    left it alone for the same reason.

Happy to fold either in if you would prefer them consistent.

Notes

  • CF_ROUTER_INJECTION is size-sensitive against the 10KB CloudFront Functions
    limit. This reuses the existing baselessUri variable, so the injected code
    gets slightly smaller.
  • Found on 4.17.0 against a live deployment, and the code is identical on
    4.17.1 and dev.

Both S3 branches match on baselessUri, then build the key from the full
request uri, so a site with a base path asks S3 for <dir><base><path>
while its assets are uploaded at <dir><path>. Every static asset 404s,
surfacing as 403 AccessDenied because the OAC principal cannot list.

The comment above the first branch already states the intent: files are
stored in the root.
Four of these fail on the parent commit with the base left in the key, which
is the bug: /_assets/admin/board/_next/static/css/abc123.css against assets
uploaded at /_assets/_next/static/css/abc123.css.

The other three are guards rather than reproductions. They pass either way and
exist so a future change to the key building cannot quietly break the cases
that already worked: a site with no base at all, the choice of S3 origin, and a
path the S3 routes do not match still reaching the server.

loadRouteSite now takes the same input type as createContext. It was declaring
a narrower one, so a test could not stub the KV store or observe which origin
was selected, which is what the KVS-matched file case needs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant