fix: SPA fallback via HttpExceptionFilter for non-API 404s
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Move SPA fallback from main.ts catch-all (didn't work - NestJS 404 handler runs first) to HttpExceptionFilter. Now non-API GET 404s serve index.html so client-side routing works for /dashboard/* etc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from "@nestjs/common";
|
||||
import { Response } from "express";
|
||||
import { Request, Response } from "express";
|
||||
import { join } from "path";
|
||||
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger("ExceptionFilter");
|
||||
private readonly indexPath = join(__dirname, "..", "..", "..", "..", "web", "dist", "index.html");
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
@@ -30,6 +33,11 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
this.logger.error(`Unhandled error: ${exception.message}`, exception.stack);
|
||||
}
|
||||
|
||||
// SPA fallback: serve index.html for non-API GET 404s
|
||||
if (status === HttpStatus.NOT_FOUND && request.method === "GET" && !request.path.startsWith("/api")) {
|
||||
return response.sendFile(this.indexPath);
|
||||
}
|
||||
|
||||
// Record error on active OTel span
|
||||
const span = trace.getActiveSpan();
|
||||
if (span) {
|
||||
|
||||
@@ -3,14 +3,12 @@ import "./telemetry/tracing"; // MUST be first — instruments modules before th
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import helmet from "helmet";
|
||||
import { join } from "path";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { AppModule } from "./app.module";
|
||||
import { fileUploadValidation } from "./common/middleware/file-upload-validation.middleware";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
const port = configService.get<number>("port", 4000);
|
||||
@@ -54,18 +52,6 @@ async function bootstrap() {
|
||||
next();
|
||||
});
|
||||
|
||||
await app.init();
|
||||
|
||||
// SPA fallback: serve index.html for non-API GET requests
|
||||
const webDistPath = join(__dirname, "..", "..", "web", "dist");
|
||||
const expressApp = app.getHttpAdapter().getInstance();
|
||||
expressApp.get("*", (req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.path.startsWith("/api")) {
|
||||
return next();
|
||||
}
|
||||
res.sendFile(join(webDistPath, "index.html"));
|
||||
});
|
||||
|
||||
await app.listen(port);
|
||||
console.log(`API running on http://localhost:${port}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user