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
7 changes: 7 additions & 0 deletions packages/angular/ssr/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 25 additions & 0 deletions packages/angular/ssr/test/app-engine_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -77,6 +79,20 @@ function createEntryPoint(locale: string) {
</html>
`,
},
'ssg-non-ascii/دليل/index.html': {
size: 35,
hash: 'a1b2c3d4e5f6',
text: async () => `<html>
<head>
<title>SSG non-ascii page</title>
<base href="/${locale}" />
</head>
<body>
SSG non-ascii works ${locale.toUpperCase()}
</body>
</html>
`,
},
},
locale,
);
Expand Down Expand Up @@ -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);
Expand Down
169 changes: 132 additions & 37 deletions packages/angular/ssr/test/app_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -140,6 +145,21 @@ describe('AngularServerApp', () => {
size: 28,
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
},
'home-ssg-non-ascii/دليل/index.html': {
text: async () =>
`<html>
<head>
<title>SSG non-ascii page</title>
<base href="/" />
</head>
<body>
<app-root>Home SSG non-ascii works</app-root>
</body>
</html>
`,
size: 38,
hash: 'a1b2c3d4e5f6',
},
},
undefined,
undefined,
Expand All @@ -152,6 +172,10 @@ describe('AngularServerApp', () => {
);

app = new AngularServerApp();
}

beforeAll(() => {
setupManifest();
});
Comment thread
alan-agius4 marked this conversation as resolved.

describe('handle', () => {
Expand Down Expand Up @@ -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 () => '<html><body>SSG with baseHref works</body></html>',
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 () => '<html><body>SSG with non-ASCII baseHref works</body></html>',
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() ?? [];
Expand Down