fix(core): Fix health endpoint cross-origin requests (#28878)

This commit is contained in:
Daria 2026-04-22 12:55:34 +02:00 committed by GitHub
parent 2c60f42fd0
commit b6eabb4ce5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 36 additions and 3 deletions

View File

@ -137,14 +137,16 @@ export abstract class AbstractServer {
const healthPath = this.endpointHealth;
const readinessPath = `${healthPath}/readiness`;
const healthMiddlewares = inDevelopment ? [corsMiddleware] : [];
// main health check should not care about DB connections
this.app.get(healthPath, (_req, res) => {
this.app.get(healthPath, ...healthMiddlewares, (_req, res) => {
res.send({ status: 'ok' });
});
const { connectionState } = this.dbConnection;
this.app.get(readinessPath, (_req, res) => {
this.app.get(readinessPath, ...healthMiddlewares, (_req, res) => {
const { connected, migrated } = connectionState;
if (connected && migrated && this.fullyReady) {
res.status(200).send({ status: 'ok' });

View File

@ -6,6 +6,7 @@ import merge from 'lodash/merge';
import { useBackendStatus } from './useBackendStatus';
import { useBackendConnectionStore } from '@/app/stores/backendConnection.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { defaultSettings } from '@/__tests__/defaults';
const mockStartHeartbeat = vi.fn();
@ -96,6 +97,27 @@ describe('useBackendStatus', () => {
expect(mockStopHeartbeat).toHaveBeenCalled();
});
it('should prepend backend origin to health URL when baseUrl is a full URL', async () => {
const rootStore = useRootStore();
vi.spyOn(rootStore, 'baseUrl', 'get').mockReturnValue('http://localhost:5678/');
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ status: 'ok' }),
});
const wrapper = createWrapper();
await vi.waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith('http://localhost:5678/internal/health', {
cache: 'no-store',
signal: expect.any(AbortSignal),
});
});
wrapper.unmount();
});
it('should skip health checks in preview mode', async () => {
settingsStore.setSettings(
merge({}, defaultSettings, {

View File

@ -2,6 +2,7 @@ import { onMounted, onUnmounted, ref } from 'vue';
import { useBackendConnectionStore } from '@/app/stores/backendConnection.store';
import { useHeartbeat } from '@/app/push-connection/useHeartbeat';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useRootStore } from '@n8n/stores/useRootStore';
const HEALTH_CHECK_INTERVAL = 10000;
const HEALTH_CHECK_TIMEOUT = 5000;
@ -9,6 +10,7 @@ const HEALTH_CHECK_TIMEOUT = 5000;
export function useBackendStatus() {
const backendConnectionStore = useBackendConnectionStore();
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const checking = ref(false);
/**
@ -22,8 +24,15 @@ export function useBackendStatus() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
// When baseUrl is a full URL (e.g. http://localhost:5678/ in dev mode),
// extract the origin so the health request targets the backend directly.
// When baseUrl is a relative path (e.g. /), the request goes to the current host.
const base = rootStore.baseUrl;
const origin = base.startsWith('http') ? new URL(base).origin : '';
const healthUrl = origin + settingsStore.endpointHealth;
try {
const response = await fetch(settingsStore.endpointHealth, {
const response = await fetch(healthUrl, {
cache: 'no-store',
signal: controller.signal,
});