From abbd489163329fea69563293d564ec412d1d2a6e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 07:54:02 +0000 Subject: [PATCH] =?UTF-8?q?fix(rest):=20give=20the=20bare=20501=20exits=20?= =?UTF-8?q?a=20machine=20code=20=E2=80=94=20NOT=5FIMPLEMENTED=20(#4067)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most REST error exits already carry a typed `code`, and the clone / search 501s answer `{ error, code: 'NOT_IMPLEMENTED' }`. Four 501 exits still returned a bare `{ error: '' }`, so a client could only key on the prose: - the cross-object transactional batch route (POST {basePath}/batch) when the runtime has no transaction() — the last untyped exit on that route, whose siblings (BATCH_NOT_ATOMIC, VALIDATION_FAILED, enforceBatchSize's BATCH_TOO_LARGE) were already typed by the #3897 / #3933 / #3939 line; - the two saveMetaItem-unsupported exits; - the UI-view-resolution-unsupported exit. Each now carries code: 'NOT_IMPLEMENTED', matching clone / search. Additive only (message and status unchanged), so existing clients are unaffected. Extended the cross-object batch 501 test to assert the code. Closes #4067. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TzLE9cw4gZKNyPN2ZP4iTt --- .changeset/cross-object-batch-501-code.md | 22 +++++++++++++++++++ packages/rest/src/rest-batch-endpoint.test.ts | 4 +++- packages/rest/src/rest-server.ts | 10 +++++---- 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .changeset/cross-object-batch-501-code.md diff --git a/.changeset/cross-object-batch-501-code.md b/.changeset/cross-object-batch-501-code.md new file mode 100644 index 0000000000..5ebeada3e7 --- /dev/null +++ b/.changeset/cross-object-batch-501-code.md @@ -0,0 +1,22 @@ +--- +"@objectstack/rest": patch +--- + +fix(rest): give the bare 501 error exits a machine `code` (#4067) + +Most REST error exits already carry a typed `code` (`VALIDATION_FAILED`, +`BATCH_NOT_ATOMIC`, `BATCH_TOO_LARGE`, `PERMISSION_DENIED`), and the clone / +search 501s already answer `{ error, code: 'NOT_IMPLEMENTED' }`. Four 501 exits +still returned a bare `{ error: '' }` with no code, so a client could +only key on the prose: + +- the cross-object transactional batch route (`POST {basePath}/batch`) when the + runtime has no `transaction()` — the last untyped exit on that route, whose + siblings (`BATCH_NOT_ATOMIC`, `VALIDATION_FAILED`, the `enforceBatchSize` + `BATCH_TOO_LARGE`) were already typed by the #3897 / #3933 / #3939 line; +- the two `saveMetaItem`-unsupported exits; +- the UI-view-resolution-unsupported exit. + +Each now carries `code: 'NOT_IMPLEMENTED'`, matching the clone / search 501s. +Additive only — the `error` message is unchanged and no status changes — so +existing clients are unaffected; new ones can branch on the code. diff --git a/packages/rest/src/rest-batch-endpoint.test.ts b/packages/rest/src/rest-batch-endpoint.test.ts index cd22f9e7b2..5006070dd2 100644 --- a/packages/rest/src/rest-batch-endpoint.test.ts +++ b/packages/rest/src/rest-batch-endpoint.test.ts @@ -109,10 +109,12 @@ describe('POST {basePath}/batch — cross-object transactional batch', () => { expect(route!.metadata?.tags).toEqual(expect.arrayContaining(['data', 'batch'])); }); - it('returns 501 when the runtime has no transactional ObjectQL', async () => { + it('returns 501 with a NOT_IMPLEMENTED code when the runtime has no transactional ObjectQL', async () => { const { route } = buildServer({}); // no ql provider const res = await post(route, { operations: [{ object: 'account', data: {} }] }); expect(res.statusCode).toBe(501); + // Typed like every other 501 (#4067) so clients key on the code, not the prose. + expect(res.body.code).toBe('NOT_IMPLEMENTED'); }); // ── happy path ─────────────────────────────────────────────────────────── diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 21af7717ac..c8d4b940fb 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -3334,7 +3334,7 @@ export class RestServer { const environmentId = isScoped ? req.params?.environmentId : undefined; const p = await this.resolveProtocol(environmentId, req); if (!p.saveMetaItem) { - res.status(501).json({ error: 'Save operation not supported by protocol implementation' }); + res.status(501).json({ error: 'Save operation not supported by protocol implementation', code: 'NOT_IMPLEMENTED' }); return; } @@ -3717,7 +3717,7 @@ export class RestServer { const environmentId = isScoped ? req.params?.environmentId : undefined; const p = await this.resolveProtocol(environmentId, req); if (!p.saveMetaItem) { - res.status(501).json({ error: 'Save operation not supported by protocol implementation' }); + res.status(501).json({ error: 'Save operation not supported by protocol implementation', code: 'NOT_IMPLEMENTED' }); return; } @@ -3780,7 +3780,7 @@ export class RestServer { } as any); res.json(view); } else { - res.status(501).json({ error: 'UI View resolution not supported by protocol implementation' }); + res.status(501).json({ error: 'UI View resolution not supported by protocol implementation', code: 'NOT_IMPLEMENTED' }); } } catch (error: any) { logError("[REST] Unhandled error:", error); @@ -6877,7 +6877,9 @@ export class RestServer { if (this.enforceAuth(req, res, context)) return; const ql = this.objectQLProvider ? await this.objectQLProvider(environmentId) : undefined; if (!ql || typeof ql.transaction !== 'function') { - res.status(501).json({ error: 'Transactional batch not supported by this runtime' }); + // Typed like every other 501 on this server (clone/search, + // #4067) so a client can key on the code, not the prose. + res.status(501).json({ error: 'Transactional batch not supported by this runtime', code: 'NOT_IMPLEMENTED' }); return; }