Skip to content

Wire protocol gaps: silently dropped query filters, DELETE-with-body, missing discovery fields, getType round trip per write #56

Description

@cuibonobo

Problem

A batch of wire-level gaps where the adapter or spec quietly delivers less than the query/API surface promises. Individually small; grouped because they're all "the wire contract lies by omission" and #52's conformance fixtures are the natural enforcement point for the lot.

1. relatedTo.label is silently dropped

StackQuery supports relatedTo: { recordId, label? } (spec §Queries), but buildQueryParams serializes only the record ID:

if (f.relatedTo) p.set('relatedTo', f.relatedTo.recordId);

(packages/adapter-api/src/index.ts:157) — and the spec's GET /records param list has no label param either. A query for "records related to X as author" returns records related to X under any label. Superset result, no error, no signal. Fix: add relatedToLabel to the spec's param list and send it.

2. content filters silently dropped on non-contentFieldQuery servers

When capabilities.contentFieldQuery is false, queryRecords falls back to GET /records with buildQueryParams — which has no handling for filter.content at all. The content filter evaporates and the server returns everything else that matched.

This is the dangerous one: the caller wrote a narrower query than what ran, and gets a superset presented as the filtered result. Any app logic that trusts the filter (dedup checks, "does a record with this slug exist", anything selection-sensitive) is wrong on exactly the adapters that can't be blamed for it. Silent capability degradation on a write of trust is the same shape #50 fixed for pagination.

Fix: fail loudly, don't widen silently. If filter.content is present and the server lacks contentFieldQuery, throw (capability error naming the missing capability) rather than degrade. Client-side post-filtering is tempting but interacts badly with pagination (a page of 50 post-filtered to 3 breaks limit/cursor semantics — the #50 discussion applies); apps that want it can do it deliberately. Same audit for search when fullTextSearch is false — today it's serialized unconditionally and behavior is whatever the server does with an unsupported param.

3. DELETE /records/:id/associations takes its payload in the body

The spec itself specifies it (§Associations: "remove an association (by body)") and dissociate() implements it (packages/adapter-api/src/index.ts:359–361). DELETE request bodies have no defined semantics (RFC 9110 §9.3.5: "has no generally defined semantics; …might lead some implementations to reject the request") — proxies, caches, gateways, and some fetch stacks drop or reject them. For a protocol meant to be implemented by arbitrary servers behind arbitrary localhost/reverse-proxy setups, this is a portability landmine in the spec, not just the adapter. Fix: POST /records/:id/associations/delete (or encode kind/label/payload discriminants as query params on DELETE). Wire-shape change, so cheapest now while the ecosystem is one server.

4. Discovery omits the attachment size limit

The spec's error table defines 413: attachment upload exceeds the server's size limit — but the discovery document (§Wire protocol, /.well-known/stack) has no field exposing that limit, and AdapterCapabilities (packages/core/src/types.ts:312–316) can't carry it. Clients learn the ceiling only by uploading the whole payload and failing. Add maxAttachmentBytes (or null = unlimited) to discovery + AdapterCapabilities, so apps can pre-check and surface limits in UI before burning the upload.

5. Every create()/update() costs an extra HTTP round trip for getType

Stack.create() and Stack.update() each call adapter.getType(...) to validate content (packages/core/src/stack.ts:377, 472). Through APIAdapter that's a GET /types/:id per write, doubling write latency. Versioned type IDs are immutable once defined — the schema-drift guard direction (backlog; schemaHash exists exactly to enforce this) makes typeId → schema a cacheable fact. Fix: a type cache in Stack (or the adapter), populated on first fetch and by defineType/saveType, invalidated never (immutable) or on explicit listTypes refresh. Owner priority says writes may be slower than reads, but this is a free 2× on every write and a per-write load on the server for a value that cannot change.

Proposed fix (summary)

  1. Spec + adapter: relatedToLabel param.
  2. Adapter: throw on content filter (and audit search) when the capability is absent — never silently widen.
  3. Spec + adapter: replace DELETE-with-body for dissociation.
  4. Spec + core types + adapter: maxAttachmentBytes in discovery/capabilities.
  5. Core: type cache keyed on versioned typeId.

Items 1–4 get #52 conformance fixtures (round-trip each query filter; dissociate through a body-stripping proxy simulation is overkill — asserting the wire shape is enough; discovery field presence). Item 5 is a core-only perf fix with a unit test that create×N hits getType once.

Non-goals

  • Full capability-negotiation framework — the existing boolean capabilities are fine; this is about honoring them loudly.
  • Offline queueing / request coalescing (explicitly deferred in adapter-api's charter).

Refs #52 (conformance fixtures; endpoint-splitting touches the same adapter surface), #50 (the pagination analogue of "silently narrower/wider than asked"), #46 (native adapter will re-run the same capability matrix), future schema-drift guard (type immutability assumption behind the cache).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions