Skip to content

Commit ecb39d2

Browse files
hotlongCopilot
andcommitted
rest: route all error paths through mapDataError sanitizer (M10.14)
Most CRUD handlers already used mapDataError, but the bulk endpoints (batch/createMany/updateMany/deleteMany), metadata read/save/delete, UI view resolution, and discovery all bare-bounced 'error.message' directly to the client. That leaked driver-level dumps like: SqliteError: insert into sys_user (...) values (...) returning * ^^^^^^^^^^^^^ table layout …and bypassed the validation/permission envelopes M10.4 introduced. Add a single sendError(res, error, object?) helper that: - honors structured { status, code } errors (e.g. 422 from metadata spec validators) but caps message length and strips stack noise - otherwise delegates to mapDataError, which already handles VALIDATION_FAILED, PERMISSION_DENIED, object-not-found, unique-violation -> 409, and the catch-all SQL-leak sanitizer (DATABASE_ERROR 500 with no SQL in the body) Replace 11 bare 'res.status(...).json({ error: error.message })' sites across registerDiscoveryRoutes/registerMetadataEndpoints/ registerUiEndpoints/registerCrudEndpoints. CRUD endpoints that already used mapDataError directly are unchanged. Verified end-to-end against running app-crm: * Force missing-column on sys_user insert: client gets {error:'Internal data error', code:'DATABASE_ERROR'} (500), server log keeps the raw SqliteError for ops triage. * Unknown object -> 404 object_not_found. * Validation failure on POST /data/lead and /data/lead/createMany -> 400 VALIDATION_FAILED with per-field array (envelope from M10.4 preserved). * Invalid metadata save -> 422 invalid_metadata (typed status pass- through still works). * All 53 @objectstack/rest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8bb6564 commit ecb39d2

1 file changed

Lines changed: 38 additions & 21 deletions

File tree

packages/rest/src/rest-server.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,31 @@ function mapDataError(error: any, object?: string): { status: number; body: Reco
133133
return { status: 400, body: { error: raw || 'Bad request' } };
134134
}
135135

136+
/**
137+
* Centralized error responder for all REST handlers. Ensures raw driver
138+
* messages (SQLite/Postgres dumps, stack traces, unique-constraint
139+
* payloads with table names, etc.) never reach clients. Honors
140+
* structured errors that already carry an explicit `status` so callers
141+
* can surface domain-specific codes (e.g. 422 from a metadata save
142+
* validator), and routes everything else through `mapDataError` so the
143+
* security / validation / SQL-leak / unknown-object envelopes apply
144+
* uniformly across CRUD, batch, metadata, UI and discovery routes.
145+
*/
146+
function sendError(res: any, error: any, object?: string): void {
147+
if (typeof error?.status === 'number' && error.status >= 400 && error.status < 600) {
148+
const safeMsg = typeof error.message === 'string' && error.message.length < 500
149+
? error.message
150+
: 'Request failed';
151+
res.status(error.status).json({
152+
error: safeMsg,
153+
...(error.code ? { code: error.code } : {}),
154+
});
155+
return;
156+
}
157+
const mapped = mapDataError(error, object);
158+
res.status(mapped.status).json(mapped.body);
159+
}
160+
136161
/**
137162
* Whether a mapped data-error status represents an *expected* client/lifecycle
138163
* outcome (and therefore shouldn't be logged as "[REST] Unhandled error").
@@ -877,7 +902,7 @@ export class RestServer {
877902
res.json(discovery);
878903
} catch (error: any) {
879904
logError("[REST] Unhandled error:", error);
880-
res.status(500).json({ error: error.message });
905+
sendError(res, error);
881906
}
882907
};
883908

@@ -925,7 +950,7 @@ export class RestServer {
925950
res.json(types);
926951
} catch (error: any) {
927952
logError("[REST] Unhandled error:", error);
928-
res.status(500).json({ error: error.message });
953+
sendError(res, error);
929954
}
930955
},
931956
metadata: {
@@ -955,7 +980,7 @@ export class RestServer {
955980
res.json(translated);
956981
} catch (error: any) {
957982
logError("[REST] Unhandled error:", error);
958-
res.status(404).json({ error: error.message });
983+
sendError(res, error);
959984
}
960985
},
961986
metadata: {
@@ -1026,7 +1051,7 @@ export class RestServer {
10261051
}
10271052
} catch (error: any) {
10281053
logError("[REST] Unhandled error:", error);
1029-
res.status(404).json({ error: error.message });
1054+
sendError(res, error);
10301055
}
10311056
},
10321057
metadata: {
@@ -1071,11 +1096,7 @@ export class RestServer {
10711096
res.json(result);
10721097
} catch (error: any) {
10731098
logError("[REST] Unhandled error:", error);
1074-
const status = typeof error?.status === 'number' ? error.status : 400;
1075-
res.status(status).json({
1076-
error: error.message,
1077-
...(error.code ? { code: error.code } : {}),
1078-
});
1099+
sendError(res, error);
10791100
}
10801101
},
10811102
metadata: {
@@ -1108,11 +1129,7 @@ export class RestServer {
11081129
res.json(result);
11091130
} catch (error: any) {
11101131
logError("[REST] Unhandled error:", error);
1111-
const status = typeof error?.status === 'number' ? error.status : 400;
1112-
res.status(status).json({
1113-
error: error.message,
1114-
...(error.code ? { code: error.code } : {}),
1115-
});
1132+
sendError(res, error);
11161133
}
11171134
},
11181135
metadata: {
@@ -1144,7 +1161,7 @@ export class RestServer {
11441161
res.json(await this.translateMetaItem(req, req.params.type, projectId, item));
11451162
} catch (error: any) {
11461163
logError("[REST] Unhandled error:", error);
1147-
res.status(404).json({ error: error.message });
1164+
sendError(res, error);
11481165
}
11491166
},
11501167
metadata: {
@@ -1177,7 +1194,7 @@ export class RestServer {
11771194
res.json(result);
11781195
} catch (error: any) {
11791196
logError("[REST] Unhandled error:", error);
1180-
res.status(400).json({ error: error.message });
1197+
sendError(res, error);
11811198
}
11821199
},
11831200
metadata: {
@@ -1214,7 +1231,7 @@ export class RestServer {
12141231
}
12151232
} catch (error: any) {
12161233
logError("[REST] Unhandled error:", error);
1217-
res.status(404).json({ error: error.message });
1234+
sendError(res, error, req.params?.object);
12181235
}
12191236
},
12201237
metadata: {
@@ -1476,7 +1493,7 @@ export class RestServer {
14761493
res.json(result);
14771494
} catch (error: any) {
14781495
logError("[REST] Unhandled error:", error);
1479-
res.status(400).json({ error: error.message });
1496+
sendError(res, error, req.params?.object);
14801497
}
14811498
},
14821499
metadata: {
@@ -1506,7 +1523,7 @@ export class RestServer {
15061523
res.status(201).json(result);
15071524
} catch (error: any) {
15081525
logError("[REST] Unhandled error:", error);
1509-
res.status(400).json({ error: error.message });
1526+
sendError(res, error, req.params?.object);
15101527
}
15111528
},
15121529
metadata: {
@@ -1536,7 +1553,7 @@ export class RestServer {
15361553
res.json(result);
15371554
} catch (error: any) {
15381555
logError("[REST] Unhandled error:", error);
1539-
res.status(400).json({ error: error.message });
1556+
sendError(res, error, req.params?.object);
15401557
}
15411558
},
15421559
metadata: {
@@ -1566,7 +1583,7 @@ export class RestServer {
15661583
res.json(result);
15671584
} catch (error: any) {
15681585
logError("[REST] Unhandled error:", error);
1569-
res.status(400).json({ error: error.message });
1586+
sendError(res, error, req.params?.object);
15701587
}
15711588
},
15721589
metadata: {

0 commit comments

Comments
 (0)