Skip to content

Commit 53df880

Browse files
committed
Address health endpoint review follow-ups
1 parent a3e951b commit 53df880

2 files changed

Lines changed: 61 additions & 5 deletions

File tree

packages/react-on-rails-pro-node-renderer/src/worker.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ declare module 'fastify' {
8686

8787
const HEALTH_ENDPOINT_ROUTES = ['/health', '/ready'] as const;
8888
const FASTIFY_DUPLICATED_ROUTE_ERROR_CODE = 'FST_ERR_DUPLICATED_ROUTE';
89+
// TODO: Reassess the duplicated-route message format when upgrading Fastify.
8990

9091
function setHeaders(headers: ResponseResult['headers'], res: FastifyReply) {
9192
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- fixing it with `void` just violates no-void
@@ -135,14 +136,15 @@ function applyFastifyConfigWithHealthEndpointMigrationHint(
135136
} catch (error) {
136137
const conflictingPath = enableHealthEndpoints ? conflictingHealthEndpointPath(error) : undefined;
137138
if (conflictingPath) {
138-
const originalMessage = error instanceof Error ? error.message : String(error);
139139
const message =
140140
`enableHealthEndpoints registers built-in GET ${conflictingPath}, but a configureFastify callback ` +
141141
`already registered that route. Remove or rename the custom ${conflictingPath} route when migrating ` +
142142
'to the built-in health endpoints. See docs/oss/building-features/node-renderer/health-checks.md.';
143143

144144
log.error({ err: error, route: conflictingPath }, message);
145-
throw new Error(`${message} Original Fastify error: ${originalMessage}`);
145+
const migrationError = new Error(message) as Error & { cause?: unknown };
146+
migrationError.cause = error;
147+
throw migrationError;
146148
}
147149

148150
throw error;
@@ -710,6 +712,10 @@ export default function run(config: Partial<Config>) {
710712
// Liveness: 200 whenever this process can answer — i.e. the event loop is
711713
// responsive. Intentionally checks no dependencies (no bundle, Rails, or
712714
// license state) so a transient dependency issue never restarts the pod.
715+
// Safe from a rate-limiting perspective (CodeQL js/missing-rate-limiting):
716+
// this is an internal renderer service not exposed to the internet, returns
717+
// a static status string, and exposes no sensitive runtime data.
718+
// lgtm[js/missing-rate-limiting]
713719
app.get('/health', (_req, res) => {
714720
res.send({ status: 'ok' });
715721
});
@@ -720,6 +726,9 @@ export default function run(config: Partial<Config>) {
720726
// bundles responds 410 to renders until the Rails client uploads one.
721727
// With workersCount > 1 the cluster module distributes probe connections
722728
// across workers, so a probe checks one worker per request.
729+
// Safe from a rate-limiting perspective (CodeQL js/missing-rate-limiting):
730+
// same rationale as /health; this returns only a static readiness status.
731+
// lgtm[js/missing-rate-limiting]
723732
app.get('/ready', (_req, res) => {
724733
if (hasAnyVMContext()) {
725734
res.send({ status: 'ready' });

packages/react-on-rails-pro-node-renderer/tests/healthEndpoints.test.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,31 @@ const renderWithBundle = async (app: ReturnType<typeof createWorker>) => {
5656
.end();
5757
};
5858

59+
const expectHealthEndpointConflict = (routePath: '/health' | '/ready') => {
60+
let caughtError: unknown;
61+
62+
try {
63+
createWorker({ enableHealthEndpoints: true });
64+
} catch (error) {
65+
caughtError = error;
66+
}
67+
68+
expect(caughtError).toBeInstanceOf(Error);
69+
if (!(caughtError instanceof Error)) {
70+
throw new Error(`Expected ${routePath} conflict to throw an Error`);
71+
}
72+
73+
const migrationError = caughtError as Error & { cause?: unknown };
74+
expect(migrationError.message).toContain(`enableHealthEndpoints registers built-in GET ${routePath}`);
75+
expect(migrationError.message).not.toContain('Original Fastify error');
76+
expect(migrationError.cause).toBeInstanceOf(Error);
77+
if (!(migrationError.cause instanceof Error)) {
78+
throw new Error(`Expected ${routePath} conflict to preserve the Fastify error cause`);
79+
}
80+
81+
expect((migrationError.cause as { code?: unknown }).code).toBe('FST_ERR_DUPLICATED_ROUTE');
82+
};
83+
5984
describe('built-in health endpoints', () => {
6085
let app: ReturnType<typeof createWorker> | undefined;
6186

@@ -114,6 +139,20 @@ describe('built-in health endpoints', () => {
114139
expect(JSON.parse(readyRes.payload)).toEqual({ status: 'waiting_for_bundle' });
115140
});
116141

142+
test.each(['false', '0'])(
143+
'GET /health and GET /ready are not registered when env var is %s',
144+
async (envValue) => {
145+
process.env.RENDERER_ENABLE_HEALTH_ENDPOINTS = envValue;
146+
app = createWorker();
147+
148+
const healthRes = await app.inject().get('/health').end();
149+
expect(healthRes.statusCode).toBe(404);
150+
151+
const readyRes = await app.inject().get('/ready').end();
152+
expect(readyRes.statusCode).toBe(404);
153+
},
154+
);
155+
117156
test('GET /ready returns 503 before a bundle is loaded and 200 after', async () => {
118157
app = createWorker({ enableHealthEndpoints: true });
119158

@@ -154,8 +193,16 @@ describe('built-in health endpoints', () => {
154193
});
155194
});
156195

157-
expect(() => createWorker({ enableHealthEndpoints: true })).toThrow(
158-
/enableHealthEndpoints registers built-in GET \/health/,
159-
);
196+
expectHealthEndpointConflict('/health');
197+
});
198+
199+
test('reports a migration hint when a custom ready route conflicts with built-in endpoints', () => {
200+
configureFastify((fastifyApp) => {
201+
fastifyApp.get('/ready', (_req, res) => {
202+
res.send({ status: 'legacy' });
203+
});
204+
});
205+
206+
expectHealthEndpointConflict('/ready');
160207
});
161208
});

0 commit comments

Comments
 (0)