diff --git a/tests/test_interactive_pane_js.py b/tests/test_interactive_pane_js.py index 1d3dc6c0..be633033 100644 --- a/tests/test_interactive_pane_js.py +++ b/tests/test_interactive_pane_js.py @@ -830,6 +830,16 @@ if (c.includes("promote")) c = run(makeEl({ hasAttribute: (a) => a === "aria-busy" }), false, queued); if (c.includes("promote")) throw new Error("dismiss-in-flight chip must be left to its DELETE verdict: " + c); +// Null / non-object 2xx body (a misbehaving proxy answering `200 null`): the +// helper normalizes it to {} so neither call site guards — it must fall through +// to the unknown/"ok" arm and SETTLE the optimistic chip (promote), never throw +// and strand a delivered message as a connection error. (The no-op +// consumeAttachments stub cannot prevent this: data.attached_ids is evaluated to +// build the :639 call args, so against unfixed code this line throws and crashes +// the harness.) +c = run(makeEl(), false, null); +if (!c.includes("promote")) + throw new Error("null body must settle via unknown-ok, not throw: " + c); console.log("settle matrix OK"); """, encoding="utf-8", diff --git a/turnstone/console/static/coordinator/coordinator.js b/turnstone/console/static/coordinator/coordinator.js index e6da753e..6a592c97 100644 --- a/turnstone/console/static/coordinator/coordinator.js +++ b/turnstone/console/static/coordinator/coordinator.js @@ -2068,7 +2068,7 @@ function createCoordinatorPane(root, wsId, opts) { // queue_full, attachments_busy, cross_user, unknown-ok) lives in // the shared helper — ONE settle matrix for both panes; see // settleSendResponse's contract for the arm semantics. - settleSendResponse(queue, data || {}, { + settleSendResponse(queue, data, { queuedEl, optimisticEl, isBusy, diff --git a/turnstone/shared_static/composer_queue.js b/turnstone/shared_static/composer_queue.js index 0d421ff6..7922cbdf 100644 --- a/turnstone/shared_static/composer_queue.js +++ b/turnstone/shared_static/composer_queue.js @@ -544,7 +544,15 @@ export function parsePriority(text) { // busy / attachments_busy / cross_user_interjection / unknown-ok — // the panes' historical shapes, verbatim. export function settleSendResponse(queue, data, ctx) { - var status = data && data.status; + // Normalize a null / non-object 2xx body once, here at the shared + // chokepoint, so neither pane's call site has to guard it (interactive + // passed bare `data`, the coordinator passed `data || {}` — the divergence + // this helper exists to erase). In-tree /send always returns an object, so + // this only hardens against a misbehaving proxy answering e.g. `200 null`; + // without it the unknown/"ok" fall-through below would deref + // data.attached_ids and surface a delivered message as a connection error. + data = data || {}; + var status = data.status; if (status === "queued" && data.msg_id) { var queuedEl = ctx.queuedEl; if (!queuedEl && data.deferred) {