-
Notifications
You must be signed in to change notification settings - Fork 11.9k
fix(@angular/build): ensure external SSR middleware handles requests outside baseHref #31933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…outside baseHref Move the `ExternalSsrMiddleware` registration to execute before Vite's internal middlewares. This fixes an issue where custom server routes (like `/health` or `/api`) were unreachable in development mode if the application used a specific `baseHref`, as Vite would filter them out before the SSR middleware could process them. Closes angular#31896
2f4cf38 to
fe496e1
Compare
|
Hi @alessiopelliccione, thanks for your contribution. We need a slightly different approach for this as the current implementation breaks some e2e tests. |
Hi @alan-agius4, The SSR middleware was incorrectly intercepting Vite's internal requests after the recent reordering. This was causing the failures of E2E tests. I added locally a check to bypass the middleware for paths like I pushed this update alessiopelliccione@309a096 Tell me if it should be ok now, and I will squash the two commits. |
|
Hi @alessiopelliccione, I doubt that it will be enough as with your change all requests will be passed via the middleware, include those for assets in memory, this will cause assets that exist to return a 404 when using hono/fastify. Example: You can try adding a new e2e tests/legacy-cli/e2e/tests/vite/ssr-serve-base-href.ts import assert from 'node:assert';
import { writeMultipleFiles } from '../../utils/fs';
import { ng, silentNg } from '../../utils/process';
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
import { getGlobalVariable } from '../../utils/env';
export default async function () {
assert(
getGlobalVariable('argv')['esbuild'],
'This test should not be called in the Webpack suite.',
);
// Forcibly remove in case another test doesn't clean itself up.
await uninstallPackage('@angular/ssr');
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
await useSha();
await installWorkspacePackages();
await updateJsonFile('angular.json', (workspaceJson) => {
workspaceJson.projects['test-project'].architect.build.options.baseHref = '/base/';
});
await writeMultipleFiles({
'src/app/app.routes.ts': `
import { Routes } from '@angular/router';
import { Home } from './home/home';
export const routes: Routes = [
{ path: 'home', component: Home }
];
`,
'src/server.ts': `
import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node';
import express from 'express';
import { join } from 'node:path';
export function app(): express.Express {
const server = express();
const browserDistFolder = join(import.meta.dirname, '../browser');
const angularNodeAppEngine = new AngularNodeAppEngine();
server.use('/api/{*splat}', (req, res) => {
res.json({ hello: 'foo' })
});
server.use(express.static(browserDistFolder, {
maxAge: '1y',
index: 'index.html'
}));
server.use(async(req, res, next) => {
const response = await angularNodeAppEngine.handle(req);
if (response) {
writeResponseToNodeResponse(response, res);
} else {
next();
}
});
return server;
}
const server = app();
export const reqHandler = createNodeRequestHandler(server);
`,
});
await silentNg('generate', 'component', 'home');
const port = await ngServe();
await validateResponse('/base/main.js', /bootstrapApplication/);
await validateResponse('/base/home', /home works/);
await validateResponse('/api/test', /foo/);
await validate404Response('/base/api/test');
await validate404Response('/home');
await validate404Response('/main.js');
function validate404Response(pathname: string): Promise<void> {
return validateResponse(pathname, /Cannot GET/, 404);
}
async function validateResponse(pathname: string, match: RegExp, status = 200): Promise<void> {
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
assert.equal(response.status, status, `Response status for ${pathname} does not match`);
const text = await response.text();
assert.match(text, match, `Response body for ${pathname} does not match`);
}
}And you can run it via Or ran and debug one of the failing tests via: |
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Move the
ExternalSsrMiddlewareregistration to execute before Vite's internal middlewares. This fixes an issue where custom server routes (like/healthor/api) were unreachable in development mode if the application used a specificbaseHref, as Vite would filter them out before the SSR middleware could process them.Closes #31896
PR Checklist
Please check to confirm your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
The
ExternalSsrMiddlewareis currently registered via the returned callback inconfigureServer, placing it after Vite's internal middlewares in the stack.Consequently, if a
baseHrefis configured (e.g.,/app/), Vite intercepts requests matching the root or other paths before they reach the custom SSR server.This prevents custom handlers in
server.ts(such as/pingor/health) from processing requests that fall outside the application's base path.This is causing issues like #31896
What is the new behavior?
The
ExternalSsrMiddlewareis registered immediately within theconfigureServerhook, ensuring it executes before Vite's internal middlewares. This allows the custom SSR server to intercept and handle requests first, enabling support for routes defined outside the configuredbaseHrefwhile allowing unhandled requests to fall through to Vite.Does this PR introduce a breaking change?