Skip to content
Open
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
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,16 @@ If you are using Angular 20 or Angular 19 with App Engine Developer Preview:
```ts
import { AngularAppEngine, createRequestHandler } from '@angular/ssr'
import { getContext } from '@netlify/angular-runtime/context.js'
import type { Context } from '@netlify/edge-functions'

const angularAppEngine = new AngularAppEngine()

export async function netlifyAppEngineHandler(request: Request): Promise<Response> {
const context = getContext()
const context: Context = getContext()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 does getContext not automatically get Context type? If so - that probably should be fixed so getContext() is typed instead of having helper in angular-runtime and type from different package?


// Example API endpoints can be defined here.
// Uncomment and define endpoints as necessary.
// const pathname = new URL(request.url).pathname;
// if (pathname === '/api/hello') {
// if (context.url.pathname === '/api/hello') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Context usage patterns and URL pathname extraction

echo "=== Checking Context type definition and usage ==="
rg -n -A5 -B5 'Context.*url|context\.url' -g '*.{ts,js,d.ts}'

echo "=== Checking Request URL usage ==="
rg -n 'request\.url|new URL\(request' -g '*.{ts,js}'

Repository: netlify/angular-runtime

Length of output: 599


🏁 Script executed:

#!/bin/bash
# Check the actual README content at lines 131 and 157
echo "=== README.md lines around 131 ==="
sed -n '125,140p' README.md

echo ""
echo "=== README.md lines around 157 ==="
sed -n '150,165p' README.md

echo ""
echo "=== Check serverModuleHelpers.js for context/URL handling ==="
cat -n src/helpers/serverModuleHelpers.js | head -50

Repository: netlify/angular-runtime

Length of output: 3489


🏁 Script executed:

#!/bin/bash
# Check for Context type imports and definitions
echo "=== Search for Context type definition ==="
rg -n "interface Context|type Context|from '@netlify" -g '*.{ts,d.ts}' -A 3

echo ""
echo "=== Check what getContext returns ==="
rg -n "export.*getContext|function getContext" -g '*.{ts,js}' -A 5

Repository: netlify/angular-runtime

Length of output: 1053


🏁 Script executed:

#!/bin/bash
# Find all actual usages of context properties to understand the API
echo "=== All context property accesses ==="
rg -n 'context\.' -g '*.{ts,js}' | grep -v "// " | head -20

echo ""
echo "=== Check Netlify context API in any type definitions ==="
find . -name "*.d.ts" -exec grep -l "Netlify\|Context" {} \;

Repository: netlify/angular-runtime

Length of output: 462


🏁 Script executed:

#!/bin/bash
# Search more specifically for context property usage
echo "=== Look for context?.ip or context property accesses ==="
rg -n 'context\?\.' -g '*.ts'

echo ""
echo "=== Check demo files for context usage ==="
cat -n demo/server.ts

echo ""
echo "=== Check dashboard component context usage ==="
sed -n '20,30p' demo/src/app/dashboard/dashboard.component.ts

Repository: netlify/angular-runtime

Length of output: 1637


Fix README examples to use the correct URL parsing approach.

The README shows context.url.pathname in the example API endpoint checks (lines 131, 157), but the Netlify Context object does not have a url property. The actual generated templates in serverModuleHelpers.js correctly use new URL(request.url).pathname. Update the README examples to match the actual template code for consistency and correctness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 131, Update the README examples that reference
context.url.pathname to use the same URL parsing used in the runtime templates:
replace usages of context.url.pathname with new URL(request.url).pathname in the
example API endpoint checks (matching the logic in serverModuleHelpers.js);
ensure all occurrences (the examples around the earlier lines like the ones
referenced) consistently parse the request URL via new URL(request.url).pathname
so the docs match generated code.

// return Response.json({ message: 'Hello from the API' });
// }

Expand All @@ -147,14 +147,14 @@ If you are using Angular 19 and did not opt into the App Engine Developer Previe
```ts
import { CommonEngine } from '@angular/ssr/node'
import { render } from '@netlify/angular-runtime/common-engine.js'
import type { Context } from '@netlify/edge-functions'

const commonEngine = new CommonEngine()

export async function netlifyCommonEngineHandler(request: Request, context: any): Promise<Response> {
export async function netlifyCommonEngineHandler(request: Request, context: Context): Promise<Response> {
// Example API endpoints can be defined here.
// Uncomment and define endpoints as necessary.
// const pathname = new URL(request.url).pathname;
// if (pathname === '/api/hello') {
// if (context.url.pathname === '/api/hello') {
// return Response.json({ message: 'Hello from the API' });
// }

Expand Down Expand Up @@ -183,8 +183,6 @@ The [`server.ts` file](https://angular.dev/guide/ssr#configure-server-side-rende

To use the Angular Runtime while building and deploying with the CLI, you need to have `netlify-cli v26.0.0` installed (or a later version).

Please also make sure to use `ntl deploy --build` (rather than `ntl build && ntl deploy`).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this as-is seems outdated because ntl deploy does run build, current equivalent would be ntl build && ntl deploy --no-build (or something along those lines) and this probably would not work correctly, especially as we now rely also on deploy-specific urls to set up allowed hosts? So maybe instead of fully removing, this should be adjusted to not use ntl deploy --no-build?


## Getting Help

We love to hear from you so if you have questions, comments or find a bug in the
Expand Down
2,113 changes: 726 additions & 1,387 deletions demo/package-lock.json

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
},
"private": true,
"dependencies": {
"@angular/animations": "21.0.0",
"@angular/common": "21.0.0",
"@angular/compiler": "21.0.0",
"@angular/core": "21.0.0",
"@angular/forms": "21.0.0",
"@angular/platform-browser": "21.0.0",
"@angular/platform-browser-dynamic": "21.0.0",
"@angular/platform-server": "21.0.0",
"@angular/router": "21.0.0",
"@angular/ssr": "21.0.0",
"@angular/animations": "^21.2.11",
"@angular/common": "^21.2.11",
"@angular/compiler": "^21.2.11",
"@angular/core": "^21.2.11",
"@angular/forms": "^21.2.11",
"@angular/platform-browser": "^21.2.11",
"@angular/platform-browser-dynamic": "^21.2.11",
"@angular/platform-server": "^21.2.11",
"@angular/router": "^21.2.11",
"@angular/ssr": "^21.2.11",
"@netlify/angular-runtime": "file:..",
"@netlify/edge-functions": "^3.0.6",
"express": "^4.21.0",
Expand All @@ -29,9 +29,9 @@
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/build": "21.0.0",
"@angular/cli": "21.0.0",
"@angular/compiler-cli": "21.0.0",
"@angular/build": "^21.2.11",
"@angular/cli": "^21.2.11",
"@angular/compiler-cli": "^21.2.11",
"@types/express": "^4.17.21",
"@types/jasmine": "~5.1.0",
"@types/node": "^22.0.0",
Expand Down
8 changes: 6 additions & 2 deletions demo/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { AngularAppEngine, createRequestHandler } from '@angular/ssr'
import { getAllowedHosts, getTrustProxyHeaders } from '@netlify/angular-runtime/app-engine-config.js'
import { getContext } from '@netlify/angular-runtime/context.js'

const angularAppEngine = new AngularAppEngine()
const angularAppEngine = new AngularAppEngine({
allowedHosts: getAllowedHosts(),
trustProxyHeaders: getTrustProxyHeaders(),
})

export const netlifyAppEngineHandler = async (request: Request): Promise<Response> => {
export async function netlifyAppEngineHandler(request: Request): Promise<Response> {
const context = getContext()

const result = await angularAppEngine.handle(request, context)
Expand Down
52 changes: 35 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@
".": {
"default": "./src/index.js"
},
"./app-engine-config": {
"types": "./src/app-engine-config.d.ts",
"default": "./src/app-engine-config.js"
},
"./app-engine-config.js": {
"types": "./src/app-engine-config.d.ts",
"default": "./src/app-engine-config.js"
},
"./common-engine": {
"types": "./src/common-engine.d.ts",
"default": "./src/common-engine.js"
Expand Down
2 changes: 2 additions & 0 deletions src/app-engine-config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export declare function getAllowedHosts(): string[]
export declare function getTrustProxyHeaders(): string[]
44 changes: 44 additions & 0 deletions src/app-engine-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { env } from 'node:process'

export function getAllowedHosts() {
const allowedHosts = []

if (!env.DEPLOY_ID || !env.DEPLOY_PRIME_URL || !env.DEPLOY_URL || !env.SITE_ID || !env.SITE_NAME || !env.URL) {
console.warn('Missing Netlify-specific environment variable(s). `allowedHosts` config might be incomplete.')
return allowedHosts
}
Comment on lines +6 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the behavior of Angular AngularAppEngine when allowedHosts is set to an empty array?

💡 Result:

When AngularAppEngine/AngularNodeAppEngine is configured with allowedHosts: [], it treats that as “no allowed hosts are configured”. As a result, requests whose request URL hostname is not recognized as allowed will not be treated as allowed; Angular will return a 400 Bad Request (and fall back to client-side rendering only when allowedHosts is non-empty / properly authorized). This behavior is described in Angular’s SSR API docs: AngularAppEngine verifies the hostname of request.url against an authorized-host list; if the hostname is not recognized and allowedHosts is not empty, the engine returns a Client-Side Rendered version, otherwise it returns 400 Bad Request. Therefore, with allowedHosts set to the empty array, the “allowedHosts is not empty” condition is false, so the 400 path applies for disallowed/unknown hosts.[1] Angular’s option type shows allowedHosts is an optional readonly string[]; passing an empty array explicitly configures the allowlist as empty.[2]

Citations:


Consider the fallback behavior when required environment variables are missing.

When environment variables are missing, this function returns an empty allowedHosts array. According to Angular's AngularAppEngine documentation, an empty allowedHosts array causes the engine to return 400 Bad Request for any request with an unrecognized hostname, effectively blocking all SSR requests. While a warning is logged, the fallback behavior is quite restrictive. Consider whether returning undefined or another approach would provide a more appropriate fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app-engine-config.js` around lines 6 - 9, The current env-check block
returns the empty allowedHosts array when Netlify vars are missing, which causes
AngularAppEngine to reject all hostnames; change the fallback so that instead of
returning allowedHosts (empty array) you return undefined (or null) to let the
engine use its default permissive behavior, or alternatively populate
allowedHosts with a safe default (e.g., localhost and 127.0.0.1) — locate the
env check that references env.DEPLOY_ID / DEPLOY_PRIME_URL / DEPLOY_URL /
SITE_ID / SITE_NAME / URL and modify the early-return to return undefined (or
set allowedHosts to a minimal safe list) rather than the empty allowedHosts.


const deployPrimeUrlHostname = new URL(env.DEPLOY_PRIME_URL).hostname

// <subdomain>.netlify.app OR <custom-domain>
// www handling is not required as Netlify will auto-redirect
allowedHosts.push(new URL(env.URL).hostname)
// <deploy-id>--<subdomain>.netlify.app
allowedHosts.push(new URL(env.DEPLOY_URL).hostname)
// <branch-name>--<subdomain>.netlify.app or <dp-#>--<subdomain>.netlify.app (supports ADS)
allowedHosts.push(deployPrimeUrlHostname)
// <subdomain>.netlify.app
// this will be duplicated for sites without custom domain
// but it's important to have in case a site's custom domain is removed after a deploy
allowedHosts.push(`${env.SITE_NAME}.netlify.app`)
// <site-id>.netlify.app
allowedHosts.push(`${env.SITE_ID}.netlify.app`)
// <deploy-id>--<site-id>.netlify.app
allowedHosts.push(`${env.DEPLOY_ID}--${env.SITE_ID}.netlify.app`)

// we need to extract the branch name or the deploy preview number
// so we can add the subdomain as well as site-id specific URLs
// this would be required for sites using ADS so that
// we can add netlify.app URLs as well
if (deployPrimeUrlHostname.includes('--')) {
const [branchNameOrDpNumber] = deployPrimeUrlHostname.split('--')
allowedHosts.push(`${branchNameOrDpNumber}--${env.SITE_NAME}.netlify.app`)
allowedHosts.push(`${branchNameOrDpNumber}--${env.SITE_ID}.netlify.app`)
}

return allowedHosts
}

export function getTrustProxyHeaders() {
return ['x-forwarded-for']
}
6 changes: 3 additions & 3 deletions src/helpers/serverModuleHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,15 @@ export async function fixServerTs({ angularVersion, siteRoot, failPlugin, failBu
const angularRuntimeVersionInstalledByUser = await getAngularRuntimeVersion(siteRoot)
if (!angularRuntimeVersionInstalledByUser) {
failBuild(
"Angular@19 SSR on Netlify requires '@netlify/angular-runtime' version 2.2.0 or later to be installed. Please install it and try again.",
`Angular@${angularVersion} SSR on Netlify requires '@netlify/angular-runtime' version 2.2.0 or later to be installed. Please install it and try again.`,
)
} else if (!satisfies(angularRuntimeVersionInstalledByUser, '>=2.2.0', { includePrerelease: true })) {
failBuild(
`Angular@19 SSR on Netlify requires '@netlify/angular-runtime' version 2.2.0 or later to be installed. Found version "${angularRuntimeVersionInstalledByUser}". Please update it to version 2.2.0 or later and try again.`,
`Angular@${angularVersion} SSR on Netlify requires '@netlify/angular-runtime' version 2.2.0 or later to be installed. Found version "${angularRuntimeVersionInstalledByUser}". Please update it to version 2.2.0 or later and try again.`,
)
}

// check whether project is using stable CommonEngine or Developer Preview AppEngine
// check whether project is using CommonEngine or AppEngine
const serverModuleContents = await readFile(serverModuleLocation, 'utf8')

const usedEngineBasedOnKnownSignatures = getEngineBasedOnKnownSignatures(serverModuleContents)
Expand Down
10 changes: 8 additions & 2 deletions src/helpers/setUpEdgeFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,19 @@ export async function setUpEdgeFunction({ outputPath, constants, failBuild, used
toPosix,
)

// buy putting this into a separate module that's imported first,
// by putting this into a separate module that's imported first,
// we ensure this is initialised before any other module
const polyfills = `
import process from "node:process"

globalThis.process = process
globalThis.DenoEvent = globalThis.Event // storing this for fixup-event.mjs
globalThis.process = process
globalThis.process.env.DEPLOY_ID = ${JSON.stringify(env.DEPLOY_ID)}
globalThis.process.env.DEPLOY_PRIME_URL = ${JSON.stringify(env.DEPLOY_PRIME_URL)}
globalThis.process.env.DEPLOY_URL = ${JSON.stringify(env.DEPLOY_URL)}
globalThis.process.env.SITE_ID = ${JSON.stringify(env.SITE_ID)}
globalThis.process.env.SITE_NAME = ${JSON.stringify(env.SITE_NAME)}
globalThis.process.env.URL = ${JSON.stringify(env.URL)}
`

// angular's polyfills override the global `Event` with a custom implementation.
Expand Down
Loading