Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ The `forwarder` function is **tightly coupled** to the Angular build output dire

`projects/v3` is compiled with `"localize": true` in `angular.json`, producing one subfolder per locale:

| Angular version | Build output path | S3 object key prefix |
|-----------------|-------------------|----------------------|
| 17 / 18 (webpack builder) | `dist/v3/{locale}/` | `/{locale}/` |
| **19+ (application builder)** | `dist/v3/browser/{locale}/` | `/browser/{locale}/` |
| Angular version / mode | Build output path | S3 object key prefix |
|------------------------|-------------------|----------------------|
| 17 / 18 (webpack browser builder) | `dist/v3/{locale}/` | `/{locale}/` |
| 19 (application builder rollout) | `dist/v3/browser/{locale}/` | `/browser/{locale}/` |
| **20 baseline in this repo (browser builder)** | `dist/v3/{locale}/` | `/{locale}/` |

The `aws s3 sync dist/v3/ s3://$BUCKET --delete` step (CI/CD step 22) mirrors this structure directly into S3, so the S3 key prefix always matches the build output.

Expand All @@ -28,9 +29,10 @@ CloudFront does **not** forward query strings to S3 (`QueryString: false`). When

1. Extracts the first path segment as the locale (`en-US`, `ja`, `ms`, `es`).
2. For unknown locales or bare `/`, falls back to the default locale.
3. For SPA routes (no file extension), rewrites to `/{prefix}/{locale}/index.html`.
4. For static assets (with file extension), rewrites to `/{prefix}{original_uri}`.
5. Passes `version.json` requests through unchanged (whitelist).
3. For SPA routes (no file extension), rewrites to `/{locale}/index.html`.
4. For static assets (with file extension), keeps `/{locale}/asset.ext` unchanged.
5. For legacy `/browser/...` requests, strips the `/browser` prefix and rewrites to locale-root keys.
6. Passes `version.json` requests through unchanged (whitelist).

#### Supported locales

Expand All @@ -49,8 +51,8 @@ When upgrading Angular major versions, verify the build output structure has not
```bash
# after building, check what subdirectories are produced
ls dist/v3/
# Angular 19+: should show browser/
# Angular 17/18: should show en-US/ ja/ ms/ es/
# browser builder layout: should show locale folders directly
# e.g. en-US/ ja/ ms/ es/
```

If the output structure changes, update the path prefix in `forwarder/index.js` **before** merging to trunk so both changes deploy together in the same CI/CD run.
Expand All @@ -59,7 +61,18 @@ If the output structure changes, update the path prefix in `forwarder/index.js`

| Date | Trigger | Symptom | Fix |
|------|---------|---------|-----|
| March 2026 | Angular 18 → 19 upgrade | S3 `AccessDenied` XML shown to all users on `app.p2-stage.practera.com` | Added `/browser/` prefix to all rewritten URIs in `forwarder/index.js` |
| March 2026 | Angular 18 → 19 upgrade (application builder layout) | S3 `AccessDenied` XML shown to users on deep links | Added `/browser/` prefix rewrites in `forwarder/index.js` |
| June 2026 | Angular 20 baseline kept classic browser builder layout | Root and deep links returned S3 `AccessDenied` due to `/browser/...` rewrite mismatch | Restored locale-root rewrites and added backward-compatible `/browser` stripping in `forwarder/index.js` |

#### quick verification matrix after deploy

run these checks after lambda + app deploy:

1. `/version.json` returns JSON (200).
2. `/` resolves to app html shell (not xml `AccessDenied`).
3. `/en-US/` resolves to app html shell.
4. `/en-US/v3/messages` resolves to app html shell.
5. `/browser/en-US/index.html` either resolves via compatibility rewrite or redirects to locale-root shell.

---

Expand Down
23 changes: 13 additions & 10 deletions lambda/forwarder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ const path = require('path');

exports.handler = async (evt) => {
const { request } = evt.Records[0].cf;
const locales = ["en-US", "ja", "ms", "es"];
const defaultLocale = "en-US";

console.log(`Original Uri: ${request.uri}`);

const uriParts = request.uri.split("/");
// backward-compatible rewrite: accept legacy /browser/* requests
// and normalize them to the locale-root key layout.
if (request.uri.startsWith('/browser/')) {
request.uri = request.uri.replace('/browser', '');
}

const locale = uriParts.length > 1 ? uriParts[1] : "";
const locales = ["en-US", "ja", "ms", "es"];
const uriParts = request.uri.split('/');
const locale = uriParts.length > 1 ? uriParts[1] : '';
const lastPartUrl = uriParts[uriParts.length - 1];

// whitelisted version.json request — note: query strings are in request.querystring,
Expand All @@ -18,18 +24,15 @@ exports.handler = async (evt) => {
return request;
}

if (locale === "" || locale === "index.html" || !locales.includes(locale)) {
request.uri = "/browser/en-US/index.html";
console.log("Go to default page and locale.");
if (locale === '' || locale === 'index.html' || !locales.includes(locale)) {
request.uri = `/${defaultLocale}/index.html`;
console.log('go to default page and locale.');
return request;
}

const fileExt = path.extname(lastPartUrl);
if (!fileExt) {
request.uri = `/browser/${locale}/index.html`;
} else if (!request.uri.startsWith('/browser/')) {
// rewrite static asset paths to match Angular 19+ application builder output
request.uri = `/browser${request.uri}`;
request.uri = `/${locale}/index.html`;
}

console.log(`New Uri: ${request.uri}`);
Expand Down
Loading