Skip to content

Commit ac69370

Browse files
committed
fix(rest,service-settings,service-datasource): adopt ADR-0112 codes and refresh the console pin (#3843)
Merging main brought in ADR-0112 batch 1 (#3841 (#3988)) and #3842, both of which landed while this PR was in review. Two of this PR's stated decisions were premised on #3841 being UNDECIDED, so they no longer hold. `ApiErrorSchema.code` is now the closed `ErrorCode` union, not `z.string()`, so the nine lowercase codes this PR deliberately carried over failed schema parse — which failed its own conformance suites (6 in service-datasource, 1 in service-settings). Re-spelled per ADR-0112, with generic conditions going to the STANDARD catalog rather than becoming registered synonyms of it, which is what the ledger asks for: datasource_admin_unavailable → SERVICE_UNAVAILABLE (standard) external_service_unavailable → SERVICE_UNAVAILABLE (standard) not_found / PACKAGE_NOT_FOUND → RESOURCE_NOT_FOUND (standard) PUBLISH_FIELDS_MISSING → MISSING_REQUIRED_FIELD (standard) INTERNAL → INTERNAL_ERROR (standard) datasource_admin_error → DATASOURCE_ADMIN_ERROR (registered) external_import_error → EXTERNAL_IMPORT_ERROR (registered) PUBLISH_MANIFEST_INVALID → PACKAGE_MANIFEST_INVALID (registered) PUBLISH_FAILED → PACKAGE_PUBLISH_FAILED (registered) PACKAGE_DELETE_PARTIAL / PACKAGE_DELETE_FAILED / SETTINGS_ACTION_FAILED Which service is unavailable is carried by `message` — the ledger explicitly asks generic conditions to reuse the catalog instead of registering a per-service 503. The seven registered codes go into ERROR_CODE_LEDGER under their owning packages, including a new @objectstack/service-datasource entry. Also bumps .objectui-sha a136322 → e651c93 now that objectui#2972 has merged, so the bundled Setup console carries the envelope tolerance this change needs. The pin only moves two commits because main had already refreshed it to a136322 — an earlier attempt from this branch's stale base would have swept in eleven unrelated frontend commits including two breaking ones. The route-envelope guard, the conformance suites and their doc comments are updated to match. Guard: 8 modules, 5 conformant / 2 ratcheted / 1 exempt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z
1 parent 0757f0f commit ac69370

12 files changed

Lines changed: 112 additions & 65 deletions

.changeset/console-e651c936870e.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/console": patch
3+
---
4+
5+
Console (objectui) refreshed to `e651c936870e`. Frontend changes in this range:
6+
7+
- fix(app-shell): unwrap the declared response envelope on the datasource page and the api-action runner (objectstack#3843) (#2972)
8+
- fix(actions): read objectstack#3962's single-wrapped /actions responses (#2971)
9+
10+
objectui range: `a136322f8723...e651c936870e`

.changeset/route-envelope-four-more-modules.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,28 @@ answered a bare `{ success: false }` and a bare
5454
`PACKAGE_DELETE_PARTIAL`, with the per-item `failed` / `cleanups` arrays under
5555
`error.details`.
5656

57-
**Codes: carried over, not renamed.** `admin-routes.ts` and
58-
`external-datasource-routes.ts` keep their existing lowercase snake codes
59-
(`datasource_admin_unavailable`, `external_service_unavailable`, `not_found`, …)
60-
even though they sit beside SCREAMING_SNAKE in the already-converted siblings.
61-
Which vocabulary wins is #3841's call — a decision about ~240 codes repo-wide —
62-
and re-spelling nine of them here would pick that dialect by accident. Only
63-
`package-routes.ts` needed *minted* codes, because its `error` strings were human
64-
messages with no code to carry; those follow the SCREAMING_SNAKE the two
65-
converted siblings emit and #3841 will re-spell them with everything else. This
66-
is the envelope only, the same split #3687 / #3837 made deliberately.
57+
**Codes follow ADR-0112.** #3841 settled the vocabulary while this was in review:
58+
`error.code` is SCREAMING_SNAKE and `ApiErrorSchema.code` is now the closed
59+
`ErrorCode` union, so an unregistered code fails schema parse. Generic conditions
60+
reuse the STANDARD catalog rather than becoming registered synonyms of it, per the
61+
ledger's own guidance:
62+
63+
```
64+
datasource_admin_unavailable → SERVICE_UNAVAILABLE (standard)
65+
external_service_unavailable → SERVICE_UNAVAILABLE (standard)
66+
not_found / PACKAGE_NOT_FOUND → RESOURCE_NOT_FOUND (standard)
67+
PUBLISH_FIELDS_MISSING → MISSING_REQUIRED_FIELD (standard)
68+
INTERNAL → INTERNAL_ERROR (standard)
69+
datasource_admin_error → DATASOURCE_ADMIN_ERROR (registered)
70+
external_import_error → EXTERNAL_IMPORT_ERROR (registered)
71+
PUBLISH_MANIFEST_INVALID → PACKAGE_MANIFEST_INVALID (registered)
72+
PUBLISH_FAILED → PACKAGE_PUBLISH_FAILED (registered)
73+
PACKAGE_DELETE_PARTIAL / PACKAGE_DELETE_FAILED / SETTINGS_ACTION_FAILED (registered)
74+
```
75+
76+
Which service is unavailable is carried by `message`. The seven registered codes are
77+
added to `ERROR_CODE_LEDGER` under their owning packages — including a new
78+
`@objectstack/service-datasource` entry.
6779

6880
**`POST /external/validate` keeps its `ok`.** Unlike the `{ ok: true, key }`
6981
#3689 retired from storage — a private second word for `success` — this `ok` is a

.objectui-sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
a136322f872314a74462ac7e7142f424e1124400
1+
e651c936870e26ef0c15252baad479d293fb3cf3

packages/rest/src/external-datasource-envelope.conformance.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* arms, no `success` flag on any body, and — on `POST /validate` — its own
99
* private success word:
1010
*
11-
* res.status(503).json({ error: 'external_service_unavailable' });
11+
* res.status(503).json({ error: 'SERVICE_UNAVAILABLE' });
1212
* res.json({ ok: results.every((r: any) => r.ok), results });
1313
*
1414
* The `ok` is the interesting one, because it is NOT the `ok` #3689 retired from
@@ -181,13 +181,13 @@ describe('external-datasource envelope (#3843) — error bodies', () => {
181181
{
182182
name: 'federation is not wired into the host',
183183
status: 503,
184-
code: 'external_service_unavailable',
184+
code: 'SERVICE_UNAVAILABLE',
185185
run: () => drive(mount(undefined), 'GET', `${EXT}/tables`),
186186
},
187187
{
188188
name: 'an import the service refuses',
189189
status: 400,
190-
code: 'external_import_error',
190+
code: 'EXTERNAL_IMPORT_ERROR',
191191
run: () => drive(
192192
mount({ importObject: async () => { throw new Error('metadata store is read-only'); } }),
193193
'POST',
@@ -239,7 +239,7 @@ describe('external-datasource envelope (#3843) — error bodies', () => {
239239
const { status, body } = await drive(routes, method, path, req);
240240
expect(status, `${method} ${path}`).toBe(503);
241241
expect(body.success, `${method} ${path}`).toBe(false);
242-
expect(body.error.code, `${method} ${path}`).toBe('external_service_unavailable');
242+
expect(body.error.code, `${method} ${path}`).toBe('SERVICE_UNAVAILABLE');
243243
}
244244
});
245245
});

packages/rest/src/external-datasource-routes.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { IHttpServer } from '@objectstack/spec/contracts';
88
*
99
* Mounted under `/api/v1/datasources/:name/external/*` and served by the
1010
* `external-datasource` service. Every route degrades gracefully
11-
* (`503 external_service_unavailable`) when federation is not wired into the
11+
* (`503 SERVICE_UNAVAILABLE`) when federation is not wired into the
1212
* host, so the routes are safe to register unconditionally.
1313
*
1414
* GET /datasources/:name/external/tables → listRemoteTables
@@ -47,9 +47,14 @@ export function registerExternalDatasourceRoutes(
4747
*
4848
* Before #3843 this module emitted the pre-#3675 `{ error: '<string>' }`,
4949
* with the message a SIBLING of `error` on the import path — so a caller
50-
* reading `body.error.message` got `undefined`. The `code` strings are carried
51-
* over unchanged; picking a vocabulary is #3841's call (see the same note on
52-
* `admin-routes.ts`).
50+
* reading `body.error.message` got `undefined`.
51+
*
52+
* Codes follow ADR-0112 (#3841, settled while this was in review):
53+
* `external_service_unavailable` → the standard `SERVICE_UNAVAILABLE`, since the
54+
* ledger asks generic conditions to reuse the catalog rather than register a
55+
* per-service synonym; `external_import_error` → `EXTERNAL_IMPORT_ERROR`,
56+
* registered under this package because a refused federated import is specific
57+
* to it.
5358
*/
5459
const sendError = (res: any, status: number, code: string, message: string) =>
5560
res.status(status).json({ success: false, error: { code, message } });
@@ -68,7 +73,7 @@ export function registerExternalDatasourceRoutes(
6873
res.status(status).json({ success: true, data });
6974

7075
const unavailable = (res: any) =>
71-
sendError(res, 503, 'external_service_unavailable', 'The external-datasource service is not available.');
76+
sendError(res, 503, 'SERVICE_UNAVAILABLE', 'The external-datasource service is not available.');
7277

7378
// List remote tables (optionally filtered by ?schema=).
7479
server.get(`${ext}/tables`, async (req: any, res: any) => {
@@ -109,7 +114,7 @@ export function registerExternalDatasourceRoutes(
109114
sendError(
110115
res,
111116
400,
112-
'external_import_error',
117+
'EXTERNAL_IMPORT_ERROR',
113118
err instanceof Error ? err.message : String(err),
114119
);
115120
}

packages/rest/src/package-envelope.conformance.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,19 +202,19 @@ describe('packages envelope (#3843) — error bodies', () => {
202202
{
203203
name: 'publish without manifest/metadata',
204204
status: 400,
205-
code: 'PUBLISH_FIELDS_MISSING',
205+
code: 'MISSING_REQUIRED_FIELD',
206206
run: () => drive(mount({}), 'POST', `${PKGS}/publish`, { body: {} }),
207207
},
208208
{
209209
name: 'publish with a manifest missing id/version',
210210
status: 400,
211-
code: 'PUBLISH_MANIFEST_INVALID',
211+
code: 'PACKAGE_MANIFEST_INVALID',
212212
run: () => drive(mount({}), 'POST', `${PKGS}/publish`, { body: { manifest: {}, metadata: {} } }),
213213
},
214214
{
215215
name: 'a publish the service refuses',
216216
status: 400,
217-
code: 'PUBLISH_FAILED',
217+
code: 'PACKAGE_PUBLISH_FAILED',
218218
run: () => drive(
219219
mount({ publish: async () => ({ success: false, error: 'version already published' }) }),
220220
'POST',
@@ -225,7 +225,7 @@ describe('packages envelope (#3843) — error bodies', () => {
225225
{
226226
name: 'reading a package that does not exist',
227227
status: 404,
228-
code: 'PACKAGE_NOT_FOUND',
228+
code: 'RESOURCE_NOT_FOUND',
229229
run: () => drive(
230230
mount({ get: async () => undefined }),
231231
'GET',
@@ -272,7 +272,7 @@ describe('packages envelope (#3843) — error bodies', () => {
272272
// unreachable that way (pinned below). `GET /:id` has no inner catch.
273273
name: 'an unexpected throw from the package service',
274274
status: 500,
275-
code: 'INTERNAL',
275+
code: 'INTERNAL_ERROR',
276276
run: () => drive(
277277
mount({ get: async () => { throw new Error('db down'); } }),
278278
'GET',

packages/rest/src/package-routes.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,16 @@ export interface PackageRoutesOptions {
6161
* Two of them carried no error at all, only a bare `{ success: false }`, so a
6262
* caller was told it failed and never told why.
6363
*
64-
* Because there were no codes here to preserve, these had to be MINTED — unlike
65-
* `admin-routes.ts` / `external-datasource-routes.ts`, whose existing codes are
66-
* carried over untouched so #3841 can pick a vocabulary for the whole repo at
67-
* once. The dialect chosen is the SCREAMING_SNAKE that the two already-converted
68-
* siblings (`storage-routes.ts`, `settings-routes.ts`) emit, on the grounds that
69-
* it adds no new information to #3841's decision; expect that sweep to re-spell
70-
* them along with everything else.
64+
* Because there were no codes here to preserve, these had to be MINTED. They
65+
* follow ADR-0112 (#3841, settled while this was in review): SCREAMING_SNAKE, and
66+
* registered in `ERROR_CODE_LEDGER` under `@objectstack/rest` — an unregistered
67+
* code fails `ApiErrorSchema` parse, which fails the conformance suite.
68+
*
69+
* Generic conditions reuse the STANDARD catalog rather than becoming registered
70+
* synonyms of it: a missing request field is `MISSING_REQUIRED_FIELD`, an absent
71+
* package is `RESOURCE_NOT_FOUND`, an unexpected throw is `INTERNAL_ERROR`. Only
72+
* the package-specific outcomes are registered — `PACKAGE_MANIFEST_INVALID`,
73+
* `PACKAGE_PUBLISH_FAILED`, `PACKAGE_DELETE_PARTIAL`, `PACKAGE_DELETE_FAILED`.
7174
*/
7275
function sendError(res: any, status: number, code: string, message: string, details?: unknown) {
7376
res.status(status).json({
@@ -104,12 +107,12 @@ export function registerPackageRoutes(
104107
const { manifest, metadata } = req.body || {};
105108

106109
if (!manifest || !metadata) {
107-
sendError(res, 400, 'PUBLISH_FIELDS_MISSING', 'Missing required fields: manifest, metadata');
110+
sendError(res, 400, 'MISSING_REQUIRED_FIELD', 'Missing required fields: manifest, metadata');
108111
return;
109112
}
110113

111114
if (!manifest.id || !manifest.version) {
112-
sendError(res, 400, 'PUBLISH_MANIFEST_INVALID', 'Invalid manifest: id and version are required');
115+
sendError(res, 400, 'PACKAGE_MANIFEST_INVALID', 'Invalid manifest: id and version are required');
113116
return;
114117
}
115118

@@ -126,9 +129,9 @@ export function registerPackageRoutes(
126129
return;
127130
}
128131

129-
sendError(res, 400, 'PUBLISH_FAILED', result.error ?? `Failed to publish ${manifest.id}.`);
132+
sendError(res, 400, 'PACKAGE_PUBLISH_FAILED', result.error ?? `Failed to publish ${manifest.id}.`);
130133
} catch (error) {
131-
sendError(res, 500, 'INTERNAL', (error as Error).message);
134+
sendError(res, 500, 'INTERNAL_ERROR', (error as Error).message);
132135
}
133136
});
134137

@@ -181,7 +184,7 @@ export function registerPackageRoutes(
181184
const packages = Array.from(packagesMap.values());
182185
sendOk(res, { packages, total: packages.length });
183186
} catch (error) {
184-
sendError(res, 500, 'INTERNAL', (error as Error).message);
187+
sendError(res, 500, 'INTERNAL_ERROR', (error as Error).message);
185188
}
186189
});
187190

@@ -214,9 +217,9 @@ export function registerPackageRoutes(
214217
}
215218
}
216219

217-
sendError(res, 404, 'PACKAGE_NOT_FOUND', `Package "${packageId}" was not found.`);
220+
sendError(res, 404, 'RESOURCE_NOT_FOUND', `Package "${packageId}" was not found.`);
218221
} catch (error) {
219-
sendError(res, 500, 'INTERNAL', (error as Error).message);
222+
sendError(res, 500, 'INTERNAL_ERROR', (error as Error).message);
220223
}
221224
});
222225

@@ -276,7 +279,7 @@ export function registerPackageRoutes(
276279
`Failed to delete ${packageId}${version ? `@${version}` : ''}.`,
277280
);
278281
} catch (error) {
279-
sendError(res, 500, 'INTERNAL', (error as Error).message);
282+
sendError(res, 500, 'INTERNAL_ERROR', (error as Error).message);
280283
}
281284
});
282285
}

packages/services/service-datasource/src/__tests__/admin-routes.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ describe('registerDatasourceAdminRoutes (real HonoHttpServer)', () => {
145145
expect(await res.json()).toEqual({
146146
success: false,
147147
error: {
148-
code: 'datasource_admin_unavailable',
148+
code: 'SERVICE_UNAVAILABLE',
149149
message: 'The datasource-admin service is not available.',
150150
},
151151
});
@@ -167,7 +167,7 @@ describe('registerDatasourceAdminRoutes (real HonoHttpServer)', () => {
167167
// asymmetry that made `body.error.message` read `undefined` here.
168168
expect(await res.json()).toEqual({
169169
success: false,
170-
error: { code: 'datasource_admin_error', message: 'duplicate name' },
170+
error: { code: 'DATASOURCE_ADMIN_ERROR', message: 'duplicate name' },
171171
});
172172
});
173173
});

packages/services/service-datasource/src/__tests__/envelope.conformance.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* merely a missing `success` flag, but the pre-#3675 `{ error: '<string>' }`,
88
* with the message a SIBLING of `error` rather than a field of it —
99
*
10-
* res.status(400).json({ error: 'datasource_admin_error', message });
10+
* res.status(400).json({ error: 'DATASOURCE_ADMIN_ERROR', message });
1111
*
1212
* so a caller reading `body.error.message` got `undefined` here and the real
1313
* message from the dispatcher. That is the identical asymmetry #3675 opened on,
@@ -162,37 +162,37 @@ describe('datasource-admin envelope (#3843) — error bodies', () => {
162162
{
163163
name: 'the datasource-admin service is not wired',
164164
status: 503,
165-
code: 'datasource_admin_unavailable',
165+
code: 'SERVICE_UNAVAILABLE',
166166
run: () => drive(mount(undefined), '/api/v1/datasources'),
167167
},
168168
{
169169
name: 'a lifecycle failure carries the service message',
170170
status: 400,
171-
code: 'datasource_admin_error',
171+
code: 'DATASOURCE_ADMIN_ERROR',
172172
run: () => drive(mount({ createDatasource: async () => { throw new Error('duplicate name'); } }), '/api/v1/datasources', { method: 'POST', body: JSON.stringify({ name: 'pg' }) }),
173173
},
174174
{
175175
name: 'a missing required body field',
176176
status: 400,
177-
code: 'datasource_admin_error',
177+
code: 'DATASOURCE_ADMIN_ERROR',
178178
run: () => drive(mount({ generateObjectDraft: async () => ({}) }), '/api/v1/datasources/ext/object-draft', { method: 'POST', body: '{}' }),
179179
},
180180
{
181181
name: 'reading a datasource that does not exist',
182182
status: 404,
183-
code: 'not_found',
183+
code: 'RESOURCE_NOT_FOUND',
184184
run: () => drive(mount({ getDatasource: async () => undefined }), '/api/v1/datasources/nope'),
185185
},
186186
{
187187
name: 'a remote-table introspection failure',
188188
status: 400,
189-
code: 'datasource_admin_error',
189+
code: 'DATASOURCE_ADMIN_ERROR',
190190
run: () => drive(mount({ listRemoteTables: async () => { throw new Error('no such schema'); } }), '/api/v1/datasources/ext/remote-tables'),
191191
},
192192
{
193193
name: 'a removal failure',
194194
status: 400,
195-
code: 'datasource_admin_error',
195+
code: 'DATASOURCE_ADMIN_ERROR',
196196
run: () => drive(mount({ removeDatasource: async () => { throw new Error('not runtime-origin'); } }), '/api/v1/datasources/pg', { method: 'DELETE' }),
197197
},
198198
];

packages/services/service-datasource/src/admin-routes.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { DRIVER_CATALOG } from './driver-catalog.js';
99
*
1010
* Mounted under `/api/v1/datasources` and served by the `datasource-admin`
1111
* service. Every route degrades gracefully
12-
* (`503 datasource_admin_unavailable`) when the service is not wired in, and
12+
* (`503 SERVICE_UNAVAILABLE`) when the service is not wired in, and
1313
* lifecycle/validation failures surface as `400` with the service's message.
1414
*
1515
* GET /datasources → listDatasources (provenance + health)
@@ -66,13 +66,21 @@ export function registerDatasourceAdminRoutes(
6666
* difference; that shim is the consumer-side symptom Prime Directive #12 says
6767
* to cure at the producer.
6868
*
69-
* The `code` strings are carried over UNCHANGED (`datasource_admin_error`,
70-
* `not_found`, …) even though they are lowercase snake while
71-
* `storage-routes.ts` and `settings-routes.ts` emit SCREAMING_SNAKE. Which
72-
* vocabulary wins is #3841's call, and it is a decision about ~240 codes
73-
* repo-wide, not about these four; re-spelling them here would pick that
74-
* dialect by accident and make #3841's sweep harder to verify. This change is
75-
* the envelope only — the same split #3687 / #3837 made deliberately.
69+
* The codes follow ADR-0112, which #3841 settled while this was in review:
70+
* `error.code` is SCREAMING_SNAKE and `ApiErrorSchema.code` is now the closed
71+
* `ErrorCode` union, so an unregistered code fails schema parse. The old
72+
* lowercase trio was re-spelled accordingly, and the generic conditions went to
73+
* the STANDARD catalog rather than becoming registered synonyms of it:
74+
*
75+
* datasource_admin_unavailable → SERVICE_UNAVAILABLE (standard)
76+
* not_found → RESOURCE_NOT_FOUND (standard)
77+
* datasource_admin_error → DATASOURCE_ADMIN_ERROR (registered — a
78+
* lifecycle/validation refusal specific to
79+
* this service, so not a standard synonym)
80+
*
81+
* Which service is unavailable is carried by `message`; the ledger explicitly
82+
* asks generic conditions to reuse the catalog instead of registering a
83+
* per-service 503.
7684
*/
7785
const sendError = (res: any, status: number, code: string, message: string) =>
7886
res.status(status).json({ success: false, error: { code, message } });
@@ -90,10 +98,10 @@ export function registerDatasourceAdminRoutes(
9098
res.status(status).json({ success: true, data });
9199

92100
const unavailable = (res: any) =>
93-
sendError(res, 503, 'datasource_admin_unavailable', 'The datasource-admin service is not available.');
101+
sendError(res, 503, 'SERVICE_UNAVAILABLE', 'The datasource-admin service is not available.');
94102

95103
const badRequest = (res: any, err: unknown) =>
96-
sendError(res, 400, 'datasource_admin_error', err instanceof Error ? err.message : String(err));
104+
sendError(res, 400, 'DATASOURCE_ADMIN_ERROR', err instanceof Error ? err.message : String(err));
97105

98106
/** Split an inline `{ secret, ...draft }` body into (draft, secret). */
99107
const splitSecret = (body: any): { draft: any; secret: any } => {
@@ -151,7 +159,7 @@ export function registerDatasourceAdminRoutes(
151159
if (!svc?.getDatasource) return unavailable(res);
152160
try {
153161
const datasource = await svc.getDatasource(req.params.name);
154-
if (!datasource) return sendError(res, 404, 'not_found', `Datasource "${req.params.name}" does not exist.`);
162+
if (!datasource) return sendError(res, 404, 'RESOURCE_NOT_FOUND', `Datasource "${req.params.name}" does not exist.`);
155163
sendOk(res, { datasource });
156164
} catch (err) {
157165
badRequest(res, err);

0 commit comments

Comments
 (0)