backend: apply dynamic base path to OpenAPI docs server URLs

When deployed under a base path (e.g. /meet), the Stoplight "Try It"
requests were hitting /api/v1 instead of /meet/api/v1. This applies
the base path to the embedded OpenAPI spec's servers array at serve time,
following the same pattern used for the frontend index.html.

Also renames html-injection.utils to html-dynamic-base-path.utils and
updates function names for better wording.
This commit is contained in:
cruizba 2026-02-11 19:13:00 +01:00
parent 366632741c
commit a853aa02a2
5 changed files with 46 additions and 10 deletions

View File

@ -2,7 +2,7 @@ import { MeetRoom, MeetRoomFilters, MeetRoomStatus } from '@openvidu-meet/typing
import { inject, injectable } from 'inversify';
import { MeetRoomDocument, MeetRoomModel } from '../models/mongoose-schemas/room.schema.js';
import { LoggerService } from '../services/logger.service.js';
import { getBasePath } from '../utils/html-injection.utils.js';
import { getBasePath } from '../utils/html-dynamic-base-path.utils.js';
import { getBaseUrl } from '../utils/url.utils.js';
import { BaseRepository } from './base.repository.js';

View File

@ -17,7 +17,7 @@ import { internalMeetingRouter } from './routes/meeting.routes.js';
import { recordingRouter } from './routes/recording.routes.js';
import { internalRoomRouter, roomRouter } from './routes/room.routes.js';
import { userRouter } from './routes/user.routes.js';
import { getBasePath, getInjectedHtml } from './utils/html-injection.utils.js';
import { getBasePath, getHtmlWithBasePath, getOpenApiHtmlWithBasePath } from './utils/html-dynamic-base-path.utils.js';
import {
frontendDirectoryPath,
frontendHtmlPath,
@ -76,7 +76,7 @@ const createApp = () => {
// Public API routes
appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/docs`, (_req: Request, res: Response) =>
res.sendFile(publicApiHtmlFilePath)
res.type('html').send(getOpenApiHtmlWithBasePath(publicApiHtmlFilePath, INTERNAL_CONFIG.API_BASE_PATH_V1))
);
appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`, /*mediaTypeValidatorMiddleware,*/ roomRouter);
appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`, /*mediaTypeValidatorMiddleware,*/ recordingRouter);
@ -85,7 +85,7 @@ const createApp = () => {
if (process.env.NODE_ENV === 'development') {
// Serve internal API docs only in development mode
appRouter.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/docs`, (_req: Request, res: Response) =>
res.sendFile(internalApiHtmlFilePath)
res.type('html').send(getOpenApiHtmlWithBasePath(internalApiHtmlFilePath, INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1))
);
}
@ -104,7 +104,7 @@ const createApp = () => {
appRouter.get('/v1/openvidu-meet.js', (_req: Request, res: Response) => res.sendFile(webcomponentBundlePath));
// Serve OpenVidu Meet index.html file for all non-API routes (with dynamic base path injection)
appRouter.get(/^(?!.*\/(api|internal-api)\/).*$/, (_req: Request, res: Response) => {
res.type('html').send(getInjectedHtml(frontendHtmlPath));
res.type('html').send(getHtmlWithBasePath(frontendHtmlPath));
});
// Catch all other routes and return 404
appRouter.use((_req: Request, res: Response) =>

View File

@ -3,6 +3,7 @@ import fs from 'fs';
import { MEET_ENV } from '../environment.js';
let cachedHtml: string | null = null;
const cachedOpenApiHtml = new Map<string, string>();
let configValidated = false;
/**
@ -73,14 +74,14 @@ export function getBasePath(): string {
}
/**
* Injects runtime configuration into the index.html
* Applies runtime base path configuration to the index.html
* - Replaces the <base href="/"> tag with the configured base path
* - Injects a script with window.__OPENVIDU_MEET_CONFIG__ for frontend access
* - Adds a script with window.__OPENVIDU_MEET_CONFIG__ for frontend access
*
* @param htmlPath Path to the index.html file
* @returns The modified HTML content
*/
export function getInjectedHtml(htmlPath: string): string {
export function getHtmlWithBasePath(htmlPath: string): string {
// In production, cache the result for performance
if (process.env.NODE_ENV === 'production' && cachedHtml) {
return cachedHtml;
@ -103,9 +104,44 @@ export function getInjectedHtml(htmlPath: string): string {
return html;
}
/**
* Applies the runtime base path to the OpenAPI documentation HTML.
* Replaces the servers URL in the embedded OpenAPI spec so that "Try It" requests
* use the correct path when deployed under a base path (e.g. /meet/api/v1).
*
* @param htmlPath Path to the OpenAPI HTML file
* @param apiBasePath The API base path (e.g. /api/v1 or /internal-api/v1)
* @returns The modified HTML content
*/
export function getOpenApiHtmlWithBasePath(htmlPath: string, apiBasePath: string): string {
if (process.env.NODE_ENV === 'production' && cachedOpenApiHtml.has(htmlPath)) {
return cachedOpenApiHtml.get(htmlPath)!;
}
const basePath = getBasePath();
// Build full server URL: strip trailing slash from basePath to avoid double slashes
const fullServerUrl = basePath.replace(/\/$/, '') + apiBasePath;
let html = fs.readFileSync(htmlPath, 'utf-8');
// Replace the servers URL in the embedded OpenAPI JSON
// Matches "servers":[{"url":"<any-url>" and replaces the URL with the full path
html = html.replace(
/("servers":\[\{"url":")[^"]*(")/,
`$1${fullServerUrl}$2`
);
if (process.env.NODE_ENV === 'production') {
cachedOpenApiHtml.set(htmlPath, html);
}
return html;
}
/**
* Clears the cached HTML (useful for testing or config changes)
*/
export function clearHtmlCache(): void {
cachedHtml = null;
cachedOpenApiHtml.clear();
}

View File

@ -1,7 +1,7 @@
import { container } from '../config/dependency-injector.config.js';
import { MEET_ENV } from '../environment.js';
import { BaseUrlService } from '../services/base-url.service.js';
import { getBasePath } from './html-injection.utils.js';
import { getBasePath } from './html-dynamic-base-path.utils.js';
/**
* Returns the base URL for the application, including the configured base path.

View File

@ -31,7 +31,7 @@ import { ApiKeyService } from '../../src/services/api-key.service.js';
import { GlobalConfigService } from '../../src/services/global-config.service.js';
import { RecordingService } from '../../src/services/recording.service.js';
import { RoomScheduledTasksService } from '../../src/services/room-scheduled-tasks.service.js';
import { getBasePath } from '../../src/utils/html-injection.utils.js';
import { getBasePath } from '../../src/utils/html-dynamic-base-path.utils.js';
/**
* Constructs the full API path by prepending the base path.