Found while implementing #8086 (the 5xx message withhold on the same door, PR #8130). Recorded rather than acted on: #8086's ruling scoped the fix to 5xx messages only, and these are 4xx, so the sanitizer landing there deliberately does not touch them. Filed unassigned; nobody is on it.
The gap
#8086 closed the disclosure on sendThrownError — the exit a thrown error leaves by. Two other paths in the same registrar report failure by returning rather than throwing, so neither resolveThrownHttpError (#8016) nor the new withhold (#8086) ever sees them. Both answer 400 and both carry raw driver text.
1. POST /api/v1/packages/publish
packageService.publish swallows the driver error and hands the message back as data (packages/services/service-package/src/index.ts):
}catch(error){logger.error('Failed to publish package',errorasError);return{success: false,error: (errorasError).message};}and the handler puts it straight on the wire (packages/rest/src/package-routes.ts):
sendError(res,400,'PACKAGE_PUBLISH_FAILED',result.error??`Failed to publish ${manifest.id}.`);The statement being run is INSERT INTO sys_packages (...) ON CONFLICT(id, version) DO UPDATE SET ..., so a driver failure there produces exactly the family of text #3867's predicate exists to withhold — a constraint dump naming sys_packages and its columns, or a SQLITE_ERROR / SQLSTATE line.
Two defects, not one. The disclosure is the obvious half. The other is that a driver fault is being reported as 400 — a client error, inviting the caller to fix a request that was never the problem, and hiding a real server fault from every dashboard that buckets by status. This is precisely the class #8016 fixed for the throw path (a caller who was refused was told the platform had broken), in mirror image: here the platform breaks and the caller is told they made a mistake.
2. DELETE /api/v1/packages/:id — the partial-failure detail
protocol.deletePackage collects per-item failures with their raw messages:
}catch(e: any){failed.push({type: row.type,name: row.name,error: e?.message??'delete failed', ... });}and the handler ships that array as structured detail on a 400:
sendError(res,400,'PACKAGE_DELETE_PARTIAL',`Deleting ${packageId} left ${result.failedCount} item(s) behind.`,{details: {failed: result.failed,cleanups: result.cleanups}});Each failed[].error is whatever deleteMetaItem threw — including its Failed to delete customization overlay: ${err.message} re-wrap, which interpolates the driver line. Note this leaks through details, not message, so even a message-level withhold at this door would not reach it.
Why this is not just "extend #8086 to 4xx"
It must not be. A 4xx message is caller-facing by design and #8086's PR pins that on purpose — the protocol's [tenant_scope_required] refusal names the exact parameter to pass, a 409 DESTRUCTIVE_CHANGE names the remedy. Blanket-sanitizing 4xx would destroy the self-correcting messages #4277 exists for.
The honest fixes are the other two:
Evidence
Read from code, with the reachability of the sibling 5xx path measured. The #8086 reproduction drove a throwingpublish through this handler (confirming the catch-all exit), and separately walked a real ObjectQL engine + real ObjectStackProtocolImplementation failure through DELETE, capturing the driver line on the wire. What was NOT driven end to end is the real service-packagepublish catch above — its return-shaped path is read from source, not reproduced. Whoever takes this should force an INSERT INTO sys_packages failure and record the actual 400 body before fixing, the same way #8086 was made to earn its premise.
Found while implementing #8086 (the 5xx message withhold on the same door, PR #8130). Recorded rather than acted on: #8086's ruling scoped the fix to 5xx messages only, and these are 4xx, so the sanitizer landing there deliberately does not touch them. Filed unassigned; nobody is on it.
The gap
#8086 closed the disclosure on
sendThrownError— the exit a thrown error leaves by. Two other paths in the same registrar report failure by returning rather than throwing, so neitherresolveThrownHttpError(#8016) nor the new withhold (#8086) ever sees them. Both answer 400 and both carry raw driver text.1.
POST /api/v1/packages/publishpackageService.publishswallows the driver error and hands the message back as data (packages/services/service-package/src/index.ts):and the handler puts it straight on the wire (
packages/rest/src/package-routes.ts):The statement being run is
INSERT INTO sys_packages (...) ON CONFLICT(id, version) DO UPDATE SET ..., so a driver failure there produces exactly the family of text #3867's predicate exists to withhold — a constraint dump namingsys_packagesand its columns, or aSQLITE_ERROR/SQLSTATEline.Two defects, not one. The disclosure is the obvious half. The other is that a driver fault is being reported as 400 — a client error, inviting the caller to fix a request that was never the problem, and hiding a real server fault from every dashboard that buckets by status. This is precisely the class #8016 fixed for the throw path (
a caller who was refused was told the platform had broken), in mirror image: here the platform breaks and the caller is told they made a mistake.2.
DELETE /api/v1/packages/:id— the partial-failure detailprotocol.deletePackagecollects per-item failures with their raw messages:and the handler ships that array as structured detail on a 400:
Each
failed[].erroris whateverdeleteMetaItemthrew — including itsFailed to delete customization overlay: ${err.message}re-wrap, which interpolates the driver line. Note this leaks throughdetails, notmessage, so even a message-level withhold at this door would not reach it.Why this is not just "extend #8086 to 4xx"
It must not be. A 4xx message is caller-facing by design and #8086's PR pins that on purpose — the protocol's
[tenant_scope_required]refusal names the exact parameter to pass, a409 DESTRUCTIVE_CHANGEnames the remedy. Blanket-sanitizing 4xx would destroy the self-correcting messages #4277 exists for.The honest fixes are the other two:
publishis a 5xx, and once it is, the The direct-mount package door ships 5xx messages verbatim — no leak heuristic, unlike the dispatcher twin and unlike rest-server's own /data exit #8086 withhold covers it with no new rule. This is the contract-first reading: the status is wrong, and the disclosure is downstream of the wrong status.service-packagereturning(error as Error).messageas a caller-visible string is the same producer-side defect asmetadata-protocolinterpolating driver text, and it wants the same remedy: the driver text goes to the log (it already does —logger.erroris right there), and the caller gets a stable sentence.Evidence
Read from code, with the reachability of the sibling 5xx path measured. The #8086 reproduction drove a throwing
publishthrough this handler (confirming the catch-all exit), and separately walked a realObjectQLengine + realObjectStackProtocolImplementationfailure throughDELETE, capturing the driver line on the wire. What was NOT driven end to end is the realservice-packagepublishcatch above — its return-shaped path is read from source, not reproduced. Whoever takes this should force anINSERT INTO sys_packagesfailure and record the actual 400 body before fixing, the same way #8086 was made to earn its premise.