Skip to content

Commit 9c8cf9c

Browse files
os-zhuangclaude
andauthored
fix(packages): register PATCH /packages/:id so edit-manifest reaches the handler (#2995)
handlePackages has handled `PATCH /packages/:id` (edit a package's name / description / version) all along, but the route was never mounted in dispatcher-plugin — only GET/DELETE `/:id` and PATCH `/:id/enable|disable` were. So `PATCH /packages/:id` 405'd over HTTP and the Studio "edit package" form silently failed (surfaced dogfooding the builder header refresh). Register the missing route, mirroring the GET/DELETE `/:id` handlers. `/:id` is a single path segment, so it does not shadow the `/:id/enable|disable` routes. Adds a route-registration regression test (same class as the /ready seam bug, #2217). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e1484bc commit 9c8cf9c

2 files changed

Lines changed: 30 additions & 0 deletions

File tree

packages/runtime/src/dispatcher-plugin.routes.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ describe('createDispatcherPlugin — HTTP route registration', () => {
7373
expect(routes).toContain('GET /api/v1/ready');
7474
});
7575

76+
// Regression: PATCH /packages/:id (edit a package's manifest — name /
77+
// description / version) had a handlePackages() branch all along but NO
78+
// server.patch() registration, so it 405'd over HTTP and the Studio "edit
79+
// package" form silently failed. Same class of bug as /ready above. The
80+
// sibling /:id/enable|disable PATCH routes were mounted; the bare /:id was not.
81+
it('mounts PATCH /packages/:id so the edit-manifest form reaches dispatch()', async () => {
82+
const { server, routes } = makeFakeServer();
83+
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });
84+
await plugin.start?.(makeCtx(server));
85+
86+
expect(routes).toContain('PATCH /api/v1/packages/:id');
87+
// Sanity: the sibling routes that WERE always mounted.
88+
expect(routes).toContain('PATCH /api/v1/packages/:id/enable');
89+
expect(routes).toContain('POST /api/v1/packages');
90+
});
91+
7692
it('also mounts a known existing route (sanity that start() ran)', async () => {
7793
const { server, routes } = makeFakeServer();
7894
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });

packages/runtime/src/dispatcher-plugin.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,20 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
721721
}
722722
});
723723

724+
// Edit a package's manifest (name / description / version). The
725+
// handler for this has existed in handlePackages all along, but the
726+
// route was never registered, so PATCH /packages/:id 405'd and the
727+
// Studio "edit package" form silently failed. `/:id` is a single
728+
// segment, so this does not shadow the `/:id/enable|disable` routes.
729+
server.patch(`${prefix}/packages/:id`, async (req: any, res: any) => {
730+
try {
731+
const result = await dispatcher.handlePackages(`/${req.params.id}`, 'PATCH', req.body, req.query, { request: req });
732+
sendResult(result, res);
733+
} catch (err: any) {
734+
errorResponse(err, res);
735+
}
736+
});
737+
724738
server.patch(`${prefix}/packages/:id/enable`, async (req: any, res: any) => {
725739
try {
726740
const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, 'PATCH', {}, {}, { request: req });

0 commit comments

Comments
 (0)