#53 gave record operations a typed, round-tripping error taxonomy. The attachment/blob paths were never brought into it. Two gaps, resolved differently: C3 reuses existing types, C4 needs one new type.
C3 — blob-layer errors outside the taxonomy (reuse, no new type)
DiskBlobAdapter.getAttachment() throws plain Error("Attachment not found") and assertFileId throws plain Error (packages/blob-adapter-disk/src/index.ts:18-40). The same conditions over the wire are typed (404 / 400), so a caller can't instanceof-distinguish them locally. MemoryAdapter.getAttachment is worse — it returns empty bytes for an unknown fileId (testing.ts:304-306), a documented leniency that diverges from any sane contract.
Resolution — reuse existing classes:
- Bytes absent →
StackNotFoundError (404).
- Malformed fileId (not 64-hex) →
StackQueryError (bad_request/400) — a structurally-malformed request, exactly that class's remit. It must be this class, not a new one: the wire round-trip is one-class-per-code (deserializeError maps bad_request → StackQueryError), so a second bad_request class wouldn't survive it. (If the query-flavored name ever grates, a future rename to StackBadRequestError + alias generalizes it — out of scope here.)
No anti-oracle concern: ScopedStack.getAttachment runs canAccessFile first, so a non-owner never reaches the blob layer's not-found/bad-fileId — those surface only on the owner/full-trust path.
Work:
C4 — payload-too-large has no typed error (one new type)
maxAttachmentBytes is a first-class capability, but 413 maps to no wire code and no core class — an oversized upload surfaces as a generic APIAdapterError, and neither Stack.putAttachment() nor APIAdapter pre-checks the declared ceiling. No existing class fits: it isn't schema-invalid (not 422), a conflict, or not-found.
Resolution — add StackPayloadTooLargeError:
- New core class,
static code = 'payload_too_large', status 413.
@haverstack/wire-types: add payload_too_large to WireErrorCode; WIRE_ERROR_STATUS.payload_too_large = 413; STATUS_TO_CODE[413] = 'payload_too_large' (413 is unambiguous, so status-only reconstruction from a bodyless response works); serializeError/deserializeError branches.
- Local pre-check in
Stack.putAttachment()/putAttachmentBytes() against this.features.maxAttachmentBytes — the same hoist-into-the-invariant-layer move as C2 (issue for C1/C2). Fail fast before burning the upload; the server still enforces 413 authoritatively (local adapters declare null/unbounded → no-op). Consider the check in APIAdapter too as defense-in-depth, but the Stack-level check is the primary one.
- Spec §Error responses: note the
payload_too_large wire code on the existing 413 row.
Work:
Refs
#53 (error taxonomy round-trip this extends), #56 (maxAttachmentBytes discovery), #52 (conformance fixtures). From docs/design-assessment-2026-07.md §C3/§C4 (PR #105).
#53 gave record operations a typed, round-tripping error taxonomy. The attachment/blob paths were never brought into it. Two gaps, resolved differently: C3 reuses existing types, C4 needs one new type.
C3 — blob-layer errors outside the taxonomy (reuse, no new type)
DiskBlobAdapter.getAttachment()throws plainError("Attachment not found")andassertFileIdthrows plainError(packages/blob-adapter-disk/src/index.ts:18-40). The same conditions over the wire are typed (404 / 400), so a caller can'tinstanceof-distinguish them locally.MemoryAdapter.getAttachmentis worse — it returns empty bytes for an unknown fileId (testing.ts:304-306), a documented leniency that diverges from any sane contract.Resolution — reuse existing classes:
StackNotFoundError(404).StackQueryError(bad_request/400) — a structurally-malformed request, exactly that class's remit. It must be this class, not a new one: the wire round-trip is one-class-per-code (deserializeErrormapsbad_request→StackQueryError), so a secondbad_requestclass wouldn't survive it. (If the query-flavored name ever grates, a future rename toStackBadRequestError+ alias generalizes it — out of scope here.)No anti-oracle concern:
ScopedStack.getAttachmentrunscanAccessFilefirst, so a non-owner never reaches the blob layer's not-found/bad-fileId — those surface only on the owner/full-trust path.Work:
StackBlobAdaptererror contract in §Adapters:getAttachmentthrowsStackNotFoundErrorfor absent bytes,StackQueryErrorfor a malformed fileIdDiskBlobAdapter: replace the two plainErrors withStackNotFoundError/StackQueryError(both importable from@haverstack/core)MemoryAdapter: throwStackNotFoundErrorfor an unknown fileId instead of returning empty bytes; update the permission tests that relied on synthetic never-uploaded fileIds (most assert the access decision, which precedesgetAttachment, so fallout should be small; theNoListFilesAdapter-style subclass escape hatch remains for the deliberate cases)C4 — payload-too-large has no typed error (one new type)
maxAttachmentBytesis a first-class capability, but 413 maps to no wire code and no core class — an oversized upload surfaces as a genericAPIAdapterError, and neitherStack.putAttachment()norAPIAdapterpre-checks the declared ceiling. No existing class fits: it isn't schema-invalid (not 422), a conflict, or not-found.Resolution — add
StackPayloadTooLargeError:static code = 'payload_too_large', status 413.@haverstack/wire-types: addpayload_too_largetoWireErrorCode;WIRE_ERROR_STATUS.payload_too_large = 413;STATUS_TO_CODE[413] = 'payload_too_large'(413 is unambiguous, so status-only reconstruction from a bodyless response works);serializeError/deserializeErrorbranches.Stack.putAttachment()/putAttachmentBytes()againstthis.features.maxAttachmentBytes— the same hoist-into-the-invariant-layer move as C2 (issue for C1/C2). Fail fast before burning the upload; the server still enforces 413 authoritatively (local adapters declarenull/unbounded → no-op). Consider the check inAPIAdaptertoo as defense-in-depth, but theStack-level check is the primary one.payload_too_largewire code on the existing 413 row.Work:
StackPayloadTooLargeErrorin core + wire vocab/status/serialize/deserializeStack.putAttachment/putAttachmentBytes: pre-checkmaxAttachmentBytes, throw the new class@haverstack/conformance-fixtures: pin the 413payload_too_largewire body and status-only reconstructionStackPayloadTooLargeErrorlocally without sending; wire 413 round-trips to the same classRefs
#53 (error taxonomy round-trip this extends), #56 (maxAttachmentBytes discovery), #52 (conformance fixtures). From
docs/design-assessment-2026-07.md§C3/§C4 (PR #105).