diff --git a/packages/agent/src/routes/index.ts b/packages/agent/src/routes/index.ts index 6d626180e2..39c748581c 100644 --- a/packages/agent/src/routes/index.ts +++ b/packages/agent/src/routes/index.ts @@ -174,8 +174,8 @@ function getAiRoutes(options: Options, services: Services, aiRouter: AiRouter | } function getWorkflowExecutorRoutes(options: Options, services: Services): BaseRoute[] { - if (!options.workflowExecutorUrl) return []; - + // Always mount the route so that hitting it without `workflowExecutorUrl` configured + // returns an explicit error to the client instead of a bare 404. return [new WorkflowExecutorProxyRoute(services, options)]; } diff --git a/packages/agent/src/routes/workflow/workflow-executor-proxy.ts b/packages/agent/src/routes/workflow/workflow-executor-proxy.ts index 0fc391cffe..f45c35598a 100644 --- a/packages/agent/src/routes/workflow/workflow-executor-proxy.ts +++ b/packages/agent/src/routes/workflow/workflow-executor-proxy.ts @@ -3,6 +3,7 @@ import type { AgentOptionsWithDefaults } from '../../types'; import type KoaRouter from '@koa/router'; import type { Context } from 'koa'; +import { InternalServerError } from '@forestadmin/datasource-toolkit'; import { request as httpRequest } from 'http'; import { request as httpsRequest } from 'https'; @@ -16,12 +17,14 @@ type ForwardedHeaders = { export default class WorkflowExecutorProxyRoute extends BaseRoute { readonly type = RouteType.PrivateRoute; - private readonly executorUrl: URL; + private readonly executorUrl: URL | null; constructor(services: ForestAdminHttpDriverServices, options: AgentOptionsWithDefaults) { super(services, options); // Remove trailing slash for clean URL joining - this.executorUrl = new URL(options.workflowExecutorUrl.replace(/\/+$/, '')); + this.executorUrl = options.workflowExecutorUrl + ? new URL(options.workflowExecutorUrl.replace(/\/+$/, '')) + : null; } setupRoutes(router: KoaRouter): void { @@ -30,6 +33,12 @@ export default class WorkflowExecutorProxyRoute extends BaseRoute { } private async handleProxy(context: Context): Promise { + if (!this.executorUrl) { + throw new InternalServerError( + 'The workflow executor is not configured on this agent: the `workflowExecutorUrl` option is missing.', + ); + } + const { runId } = context.params; const isTrigger = context.method === 'POST'; const qs = context.querystring ? `?${context.querystring}` : ''; diff --git a/packages/agent/test/routes/index.test.ts b/packages/agent/test/routes/index.test.ts index d2e56b8ddb..263856ffba 100644 --- a/packages/agent/test/routes/index.test.ts +++ b/packages/agent/test/routes/index.test.ts @@ -68,8 +68,12 @@ describe('Route index', () => { expect(NATIVE_QUERY_ROUTES_CTOR).toEqual([DataSourceNativeQueryRoute]); }); + const WORKFLOW_EXECUTOR_ROUTE_SIZE = 1; const BASE_ROUTE_SIZE = - ROOT_ROUTES_CTOR.length + CAPABILITIES_ROUTES_CTOR.length + NATIVE_QUERY_ROUTES_CTOR.length; + ROOT_ROUTES_CTOR.length + + CAPABILITIES_ROUTES_CTOR.length + + NATIVE_QUERY_ROUTES_CTOR.length + + WORKFLOW_EXECUTOR_ROUTE_SIZE; describe('makeRoutes', () => { describe('when a data source without relations', () => { diff --git a/packages/agent/test/routes/workflow/workflow-executor-proxy.test.ts b/packages/agent/test/routes/workflow/workflow-executor-proxy.test.ts index d5cbad16b1..9d279b3412 100644 --- a/packages/agent/test/routes/workflow/workflow-executor-proxy.test.ts +++ b/packages/agent/test/routes/workflow/workflow-executor-proxy.test.ts @@ -73,6 +73,16 @@ describe('WorkflowExecutorProxyRoute', () => { expect(route.type).toBe(RouteType.PrivateRoute); }); + + test('should not throw when workflowExecutorUrl is not configured', () => { + expect( + () => + new WorkflowExecutorProxyRoute( + services, + factories.forestAdminHttpDriverOptions.build({ workflowExecutorUrl: null }), + ), + ).not.toThrow(); + }); }); describe('setupRoutes', () => { @@ -224,6 +234,24 @@ describe('WorkflowExecutorProxyRoute', () => { ).rejects.toThrow(); }); + test('should throw an explicit error when workflowExecutorUrl is not configured', async () => { + const route = new WorkflowExecutorProxyRoute( + services, + factories.forestAdminHttpDriverOptions.build({ workflowExecutorUrl: null }), + ); + + const context = createMockContext({ + customProperties: { params: { runId: 'run-123' } }, + }); + Object.defineProperty(context, 'url', { + value: '/_internal/workflow-executions/run-123', + }); + + await expect( + (route as unknown as { handleProxy: (ctx: unknown) => Promise }).handleProxy(context), + ).rejects.toThrow('The workflow executor is not configured on this agent'); + }); + test('should forward query params to the executor', async () => { const route = new WorkflowExecutorProxyRoute( services,