From 40cbfa63c4d0201ddead88c466e74bbf0195c86e Mon Sep 17 00:00:00 2001 From: Ashfaq <105435085+Ashfaqbs@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:06:28 +0530 Subject: [PATCH] fix: return 503 instead of 400 for computer-route service errors computerRoutes()'s error handler mapped every thrown error to HTTP 400, including domain/service-state failures that are not the client's fault: an unconfigured computer service, an unknown Dot id, a disabled permission, or the computer supervisor being unreachable all came back as 400 Bad Request. That misclassifies retryable/server-side failures as client input errors and is inconsistent with workspace-routes.ts's existing convention, which reserves 400 for actual validation problems (malformed JSON, Zod failures) and uses 503 for everything else. Also added the malformed-JSON carve-out workspace-routes.ts already has (SyntaxError from a bad request body was previously falling through to the generic case, which is still correctly 400 here, but only by coincidence of being the blanket default -- now it's explicit and covered by a regression test, matching the actions route's /dots/:id/ computer/actions endpoint which is the only one that parses JSON input beyond the route param). Covered by a new tests/computer-routes.test.ts (computer-routes.ts had no direct test coverage before this), exercising: malformed JSON (400), a Zod validation failure (400), an unconfigured service (503), and an unknown Dot id (503). --- src/server/computer-routes.ts | 18 ++++----- tests/computer-routes.test.ts | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 tests/computer-routes.test.ts diff --git a/src/server/computer-routes.ts b/src/server/computer-routes.ts index a7d6824..5ac37ab 100644 --- a/src/server/computer-routes.ts +++ b/src/server/computer-routes.ts @@ -4,17 +4,17 @@ import type { ComputerService } from './computer-service.js'; import type { ComputerAction } from '../shared/computer-types.js'; export function computerRoutes(computers: ComputerService) { const app = new Hono(); - app.onError((error, c) => - c.json( + app.onError((error, c) => { + if (error instanceof SyntaxError) + return c.json({ error: 'Invalid JSON request.' }, 400); + const isValidation = error instanceof z.ZodError; + return c.json( { - error: - error instanceof z.ZodError - ? 'Invalid computer request.' - : error.message, + error: isValidation ? 'Invalid computer request.' : error.message, }, - 400, - ), - ); + isValidation ? 400 : 503, + ); + }); app.get('/dots/:id/computer', async (c) => c.json(await computers.status(c.req.param('id'))), ); diff --git a/tests/computer-routes.test.ts b/tests/computer-routes.test.ts new file mode 100644 index 0000000..c3c0b47 --- /dev/null +++ b/tests/computer-routes.test.ts @@ -0,0 +1,69 @@ +import { afterEach, expect, it } from 'vitest'; +import { WorkspaceStore } from '../src/server/workspace.js'; +import { ComputerService } from '../src/server/computer-service.js'; +import { computerRoutes } from '../src/server/computer-routes.js'; + +const stores: WorkspaceStore[] = []; +afterEach(() => { + for (const store of stores.splice(0)) store.close(); +}); + +function fixture() { + const workspace = new WorkspaceStore(':memory:', 'owner'); + stores.push(workspace); + const id = workspace.dots()[0].id; + const config = { + baseUrl: 'https://example.com', + voiceName: 'voice', + slackUsers: [], + runtimeUrl: 'http://localhost', + }; + // No computer supervisor configured: `configured` is false, so any + // action that reaches `allowed()` fails with a plain domain error + // rather than attempting a network call. + const service = new ComputerService(workspace, config, () => false); + const app = computerRoutes(service); + return { workspace, id, app }; +} + +it('answers malformed JSON on the actions route with 400, not a service error', async () => { + const { app, id } = fixture(); + const response = await app.request(`/dots/${id}/computer/actions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{', + }); + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ error: 'Invalid JSON request.' }); +}); + +it('answers an invalid action shape with 400 from the Zod schema', async () => { + const { app, id } = fixture(); + const response = await app.request(`/dots/${id}/computer/actions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ input: {} }), + }); + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ error: 'Invalid computer request.' }); +}); + +it('answers an unconfigured computer service with 503, not 400', async () => { + const { app, id } = fixture(); + const response = await app.request(`/dots/${id}/computer/start`, { + method: 'POST', + }); + expect(response.status).toBe(503); + expect(await response.json()).toEqual({ + error: 'Computer service is not configured.', + }); +}); + +it('answers an unknown Dot id with 503, carrying the domain error message', async () => { + const { app } = fixture(); + const response = await app.request('/dots/missing/computer/start', { + method: 'POST', + }); + expect(response.status).toBe(503); + expect(await response.json()).toEqual({ error: 'Dot not found.' }); +});