Skip to content

Commit 7c8d32a

Browse files
committed
fix(rest): register static data-action routes before greedy :object/:id
The REST router (RouteManager → IHttpServer/Express adapter) matches first-registered-wins with no specificity sorting. registerDataActionEndpoints (which holds GET /data/:object/export) ran AFTER registerCrudEndpoints (which holds the greedy GET /data/:object/:id), so a request to `GET /data/<object>/export` was captured by `:object/:id` — "export" treated as a record id — returning 404 RECORD_NOT_FOUND instead of streaming the export. Reorder so the static-literal action routes register before the greedy :object/:id matcher, mirroring the existing /meta/:type/:name/references-before -/meta/:type/:name convention. The reorder is safe in both directions: registerDataActionEndpoints has no greedy 2-segment :object/:id routes, and the only per-method collision in the data namespace is GET export vs GET :id. Add a regression test asserting the export route's registration index precedes the get-by-id route's.
1 parent 4845c12 commit 7c8d32a

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

packages/rest/src/rest-server.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1552,10 +1552,22 @@ export class RestServer {
15521552
this.registerReportsEndpoints(bp);
15531553
this.registerApprovalsEndpoints(bp);
15541554
this.registerAnalyticsEndpoints(bp);
1555+
// Data-action routes (e.g. GET /data/:object/export, POST
1556+
// /data/:object/import) use static-literal action segments that
1557+
// MUST be registered BEFORE the greedy GET /data/:object/:id
1558+
// matcher in registerCrudEndpoints — the router is first-match-wins
1559+
// with no specificity sorting (see route-manager.ts), so otherwise
1560+
// a request to `.../export` is captured by `:id` and "export" is
1561+
// treated as a record id (404 RECORD_NOT_FOUND). This mirrors the
1562+
// /meta/:type/:name/references-before-/meta/:type/:name convention.
1563+
// Safe in the other direction too: registerDataActionEndpoints has
1564+
// no greedy 2-segment `:object/:id` routes (only literal actions and
1565+
// deeper `:id/clone`, `:id/shares` paths), so it cannot shadow any
1566+
// CRUD literal.
1567+
this.registerDataActionEndpoints(bp);
15551568
if (this.config.api.enableCrud) {
15561569
this.registerCrudEndpoints(bp);
15571570
}
1558-
this.registerDataActionEndpoints(bp);
15591571
if (this.config.api.enableBatch) {
15601572
this.registerBatchEndpoints(bp);
15611573
}

packages/rest/src/rest.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,29 @@ describe('RestServer', () => {
904904
// 1 header + 50 000 rows.
905905
expect(lines.length).toBe(50_001);
906906
});
907+
908+
// Regression: the router is first-match-wins with no specificity sorting,
909+
// so the static-literal `GET /data/:object/export` route MUST be registered
910+
// BEFORE the greedy `GET /data/:object/:id` matcher. Otherwise a request to
911+
// `/data/<object>/export` is captured by `:id` ("export" treated as a record
912+
// id) and 404s with RECORD_NOT_FOUND instead of streaming the export.
913+
it('registers GET /export ahead of the greedy GET /:object/:id matcher', () => {
914+
const rest = new RestServer(server as any, protocol as any);
915+
rest.registerRoutes();
916+
const routes = rest.getRoutes();
917+
918+
const exportIdx = routes.findIndex(
919+
(r: any) => r.method === 'GET' && r.path === '/api/v1/data/:object/export',
920+
);
921+
const getByIdIdx = routes.findIndex(
922+
(r: any) => r.method === 'GET' && r.path === '/api/v1/data/:object/:id',
923+
);
924+
925+
expect(exportIdx).toBeGreaterThanOrEqual(0);
926+
expect(getByIdIdx).toBeGreaterThanOrEqual(0);
927+
// First match wins → the more-specific export route must come first.
928+
expect(exportIdx).toBeLessThan(getByIdIdx);
929+
});
907930
});
908931

909932
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)