diff --git a/.changeset/host-discovery-baseurl-warnings.md b/.changeset/host-discovery-baseurl-warnings.md new file mode 100644 index 0000000000..658b24bf0c --- /dev/null +++ b/.changeset/host-discovery-baseurl-warnings.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +`HostDiscovery` now logs a warning when `backend.baseUrl` is set to a localhost address while `NODE_ENV` is `production`, and when `backend.baseUrl` is not a valid URL. diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts index 1eb2d7d42b..85cbaaa73b 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts @@ -413,6 +413,56 @@ describe('HostDiscovery', () => { ); }); + describe('backend.baseUrl warnings', () => { + const env = process.env as Record; + const originalNodeEnv = env.NODE_ENV; + + afterEach(() => { + if (originalNodeEnv) { + env.NODE_ENV = originalNodeEnv; + } else { + delete env.NODE_ENV; + } + }); + + it('warns when backend.baseUrl is a localhost URL and NODE_ENV is production', () => { + env.NODE_ENV = 'production'; + const logger = mockServices.logger.mock(); + + HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:7007', + listen: { port: 7007, host: 'localhost' }, + }, + }), + { logger }, + ); + + expect(logger.warn).toHaveBeenCalledWith( + `backend.baseUrl is set to a localhost URL and NODE_ENV is 'production'. This is likely a misconfiguration — localhost URLs are not reachable by other services in a deployed environment. Prefer setting it to a routable URL that can be resolved and reached both by your app and by other plugin deployments / services.`, + ); + }); + + it('warns when backend.baseUrl is not a valid URL', () => { + const logger = mockServices.logger.mock(); + + HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'not-a-valid-url', + listen: { port: 7007, host: 'localhost' }, + }, + }), + { logger }, + ); + + expect(logger.warn).toHaveBeenCalledWith( + `backend.baseUrl config value 'not-a-valid-url' does not appear to be a valid URL.`, + ); + }); + }); + it('only accepts SRV URLs in the internal target', async () => { expect(() => HostDiscovery.fromConfig( diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts index 18ed72a085..7e25ba66c4 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts @@ -152,6 +152,27 @@ export class HostDiscovery implements DiscoveryService { }; static fromConfig(config: RootConfigService, options?: HostDiscoveryOptions) { + // The getExternalBaseUrl implementation relies on the backend base URL + // being a valid, non-local URL that others will be able to route to. + const baseUrl = config.getString('backend.baseUrl'); + try { + const { hostname } = new URL(baseUrl); + const isLocalhost = + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '::1' || + hostname === '::'; + if (isLocalhost && process.env.NODE_ENV === 'production') { + options?.logger?.warn( + `backend.baseUrl is set to a localhost URL and NODE_ENV is '${process.env.NODE_ENV}'. This is likely a misconfiguration — localhost URLs are not reachable by other services in a deployed environment. Prefer setting it to a routable URL that can be resolved and reached both by your app and by other plugin deployments / services.`, + ); + } + } catch { + options?.logger?.warn( + `backend.baseUrl config value '${baseUrl}' does not appear to be a valid URL.`, + ); + } + const discovery = new HostDiscovery(new SrvResolvers()); discovery.#updateResolvers(config, options?.defaultEndpoints);