diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index 3e0f33be8ba7..af72ac9c44a2 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -455,12 +455,19 @@ export class AngularServerApp { */ private buildServerAssetPathFromRequest(request: Request): string { let { pathname: assetPath } = new URL(request.url); + try { + assetPath = decodeURIComponent(assetPath); + } catch { + // In case of malformed URI component, keep assetPath as is. + } + if (!assetPath.endsWith('/index.html')) { // Append "index.html" to build the default asset path. assetPath = joinUrlParts(assetPath, 'index.html'); } const { baseHref } = this.manifest; + // Check if the asset path starts with the base href and the base href is not (`/` or ``). if (baseHref.length > 1 && assetPath.startsWith(baseHref)) { // Remove the base href from the start of the asset path to align with server-asset expectations. diff --git a/packages/angular/ssr/test/app-engine_spec.ts b/packages/angular/ssr/test/app-engine_spec.ts index c4313be8a48c..eae063eec700 100644 --- a/packages/angular/ssr/test/app-engine_spec.ts +++ b/packages/angular/ssr/test/app-engine_spec.ts @@ -54,11 +54,13 @@ function createEntryPoint(locale: string) { setAngularAppTestingManifest( [ { path: 'ssg', component: SSGComponent }, + { path: 'ssg-non-ascii/دليل', component: SSGComponent }, { path: 'ssr', component: SSRComponent }, { path: '', component: HomeComponent }, ], [ { path: 'ssg', renderMode: RenderMode.Prerender }, + { path: 'ssg-non-ascii/دليل', renderMode: RenderMode.Prerender }, { path: '**', renderMode: RenderMode.Server }, ], '/' + locale, @@ -77,6 +79,20 @@ function createEntryPoint(locale: string) { `, }, + 'ssg-non-ascii/دليل/index.html': { + size: 35, + hash: 'a1b2c3d4e5f6', + text: async () => ` + + SSG non-ascii page + + + + SSG non-ascii works ${locale.toUpperCase()} + + + `, + }, }, locale, ); @@ -147,6 +163,15 @@ describe('AngularAppEngine', () => { expect(await response?.text()).toContain('SSG works IT'); }); + it('should return a served prerendered page for non-ASCII routes with correct locale', async () => { + const request = new Request( + 'https://example.com/it/ssg-non-ascii/%D8%AF%D9%84%D9%8A%D9%84', + ); + const response = await appEngine.handle(request); + expect(await response?.text()).toContain('SSG non-ascii works IT'); + expect(response?.headers?.get('Content-Language')).toBe('it'); + }); + it('should return null for requests to unknown pages in a locale', async () => { const request = new Request('https://example.com/it/unknown/page'); const response = await appEngine.handle(request); diff --git a/packages/angular/ssr/test/app_spec.ts b/packages/angular/ssr/test/app_spec.ts index 1e3d40d3ede8..f14c403106bd 100644 --- a/packages/angular/ssr/test/app_spec.ts +++ b/packages/angular/ssr/test/app_spec.ts @@ -22,58 +22,59 @@ import { setAngularAppTestingManifest } from './testing-utils'; describe('AngularServerApp', () => { let app: AngularServerApp; - beforeAll(() => { - @Component({ - selector: 'app-home', - template: `Home works`, - }) - class HomeComponent { - constructor() { - if (inject(ActivatedRoute).snapshot.data['destroyApp']) { - inject(PlatformRef).destroy(); - } + @Component({ + selector: 'app-home', + template: `Home works`, + }) + class HomeComponent { + constructor() { + if (inject(ActivatedRoute).snapshot.data['destroyApp']) { + inject(PlatformRef).destroy(); } } - - @Component({ - selector: 'app-redirect', - }) - class RedirectComponent { - constructor() { - const responseInit = inject(RESPONSE_INIT); - if (responseInit) { - responseInit.status = 308; - const headers = responseInit.headers; - if (headers) { - (headers as Headers).set('X-Redirect-Header', 'custom-value'); - } + } + + @Component({ + selector: 'app-redirect', + }) + class RedirectComponent { + constructor() { + const responseInit = inject(RESPONSE_INIT); + if (responseInit) { + responseInit.status = 308; + const headers = responseInit.headers; + if (headers) { + (headers as Headers).set('X-Redirect-Header', 'custom-value'); } - - void inject(Router).navigate([], { - queryParams: { filter: 'test' }, - }); } - } - const queryParamAdderGuard: CanActivateFn = (_route, state) => { - const urlTree = inject(Router).parseUrl(state.url); + void inject(Router).navigate([], { + queryParams: { filter: 'test' }, + }); + } + } - if (urlTree.queryParamMap.has('filter')) { - return true; - } + const queryParamAdderGuard: CanActivateFn = (_route, state) => { + const urlTree = inject(Router).parseUrl(state.url); - urlTree.queryParams = { - filter: 'test', - }; + if (urlTree.queryParamMap.has('filter')) { + return true; + } - return urlTree; + urlTree.queryParams = { + filter: 'test', }; + return urlTree; + }; + + function setupManifest(): void { setAngularAppTestingManifest( [ { path: 'home', component: HomeComponent }, { path: 'home-csr', component: HomeComponent }, { path: 'home-ssg', component: HomeComponent }, + { path: 'home-ssg-non-ascii/دليل', component: HomeComponent }, { path: 'page-with-headers', component: HomeComponent }, { path: 'page-with-status', component: HomeComponent }, { path: 'page-destroy-app', component: HomeComponent, data: { destroyApp: true } }, @@ -105,6 +106,10 @@ describe('AngularServerApp', () => { 'X-Some-Header': 'value', }, }, + { + path: 'home-ssg-non-ascii/دليل', + renderMode: RenderMode.Prerender, + }, { path: 'page-with-status', renderMode: RenderMode.Server, @@ -140,6 +145,21 @@ describe('AngularServerApp', () => { size: 28, hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', }, + 'home-ssg-non-ascii/دليل/index.html': { + text: async () => + ` + + SSG non-ascii page + + + + Home SSG non-ascii works + + + `, + size: 38, + hash: 'a1b2c3d4e5f6', + }, }, undefined, undefined, @@ -152,6 +172,10 @@ describe('AngularServerApp', () => { ); app = new AngularServerApp(); + } + + beforeAll(() => { + setupManifest(); }); describe('handle', () => { @@ -282,6 +306,77 @@ describe('AngularServerApp', () => { expect(await response?.text()).toContain('Home SSG works'); }); + it('should correctly serve prerendered page with non-ASCII path', async () => { + const response = await app.handle( + new Request('http://localhost/home-ssg-non-ascii/%D8%AF%D9%84%D9%8A%D9%84'), + ); + expect(await response?.text()).toContain('Home SSG non-ascii works'); + }); + + it(`should correctly serve prerendered page with non-ASCII path when the URL ends with 'index.html'`, async () => { + const response = await app.handle( + new Request('http://localhost/home-ssg-non-ascii/%D8%AF%D9%84%D9%8A%D9%84/index.html'), + ); + expect(await response?.text()).toContain('Home SSG non-ascii works'); + }); + + it('should correctly serve prerendered page when requested with decoded non-ASCII characters', async () => { + const response = await app.handle(new Request('http://localhost/home-ssg-non-ascii/دليل')); + expect(await response?.text()).toContain('Home SSG non-ascii works'); + }); + + it('should correctly serve prerendered page with non-ASCII path when baseHref is configured', async () => { + setAngularAppTestingManifest( + [{ path: 'home-ssg-non-ascii/دليل', component: HomeComponent }], + [ + { + path: 'home-ssg-non-ascii/دليل', + renderMode: RenderMode.Prerender, + }, + ], + '/ar/', + { + 'home-ssg-non-ascii/دليل/index.html': { + text: async () => 'SSG with baseHref works', + size: 47, + hash: '123456', + }, + }, + ); + + const customApp = new AngularServerApp(); + const response = await customApp.handle( + new Request('http://localhost/ar/home-ssg-non-ascii/%D8%AF%D9%84%D9%8A%D9%84'), + ); + expect(await response?.text()).toContain('SSG with baseHref works'); + }); + + it('should correctly serve prerendered page when baseHref contains non-ASCII characters', async () => { + setAngularAppTestingManifest( + [{ path: 'page', component: HomeComponent }], + [ + { + path: 'page', + renderMode: RenderMode.Prerender, + }, + ], + '/دليل/', + { + 'page/index.html': { + text: async () => 'SSG with non-ASCII baseHref works', + size: 57, + hash: 'abcdef', + }, + }, + ); + + const customApp = new AngularServerApp(); + const response = await customApp.handle( + new Request('http://localhost/%D8%AF%D9%84%D9%8A%D9%84/page'), + ); + expect(await response?.text()).toContain('SSG with non-ASCII baseHref works'); + }); + it('should return configured headers for pages with specific header settings', async () => { const response = await app.handle(new Request('http://localhost/home-ssg')); const headers = response?.headers.entries() ?? [];