diff --git a/src/Utils/process-history-message.ts b/src/Utils/process-history-message.ts index 0b789c40..10f28706 100644 --- a/src/Utils/process-history-message.ts +++ b/src/Utils/process-history-message.ts @@ -16,7 +16,6 @@ import { proto } from '../WAProto/runtime.ts' import type { Chat, Contact, LIDMapping, WAMessage } from '../Types/index.ts' import { WAProto } from '../Types/index.ts' import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/jid-utils.ts' -import { Boom } from './boom.ts' import { toNumber } from './generics.ts' import type { ILogger } from './logger.ts' import { downloadContentFromMessage, normalizeMessageContent } from './messages.ts' @@ -234,12 +233,17 @@ export const downloadAndProcessHistorySyncNotification = async ( return processHistoryMessage(historyMsg, logger) } -/** Extract a history-sync notification through the same wrapper normalization as upstream. */ -export const getHistoryMsg = (message: proto.IMessage): proto.Message.IHistorySyncNotification => { +/** + * Extract a history-sync notification through the same wrapper normalization as + * upstream. + * + * Returns `undefined` when the message carries none. It used to throw a Boom + * 400, which breaks the shape every caller writes against a drop-in API — + * `const h = getHistoryMsg(msg); if (!h) return` crashed instead of returning. + * "Absent" is the ordinary case here, not an error: any message that is not a + * history sync takes this path. + */ +export const getHistoryMsg = (message: proto.IMessage): proto.Message.IHistorySyncNotification | undefined => { const normalizedContent = message ? normalizeMessageContent(message) : undefined - const historySyncNotification = normalizedContent?.protocolMessage?.historySyncNotification - if (!historySyncNotification) { - throw new Boom('Message does not contain a history sync notification', { statusCode: 400 }) - } - return historySyncNotification + return normalizedContent?.protocolMessage?.historySyncNotification ?? undefined } diff --git a/src/__fuzz__/harness/divergence.ts b/src/__fuzz__/harness/divergence.ts index 5a941cfd..d19a5f79 100644 --- a/src/__fuzz__/harness/divergence.ts +++ b/src/__fuzz__/harness/divergence.ts @@ -959,25 +959,6 @@ export const KNOWN_DIVERGENCES: readonly KnownDivergence[] = [ 'The two decoders disagree about which malformed `` payloads are readable, in both directions. Upstream throws "illegal buffer" where baileyrs returns an empty message object; and for a truncated length prefix (`2a 16` with no body) baileyrs throws RangeError "premature EOF" where upstream returns `[{ participant: "" }]`. Whichever way round, a corrupt stanza becomes an empty message on one side and an exception on the other, so a caller cannot write one handler that works against both. Needs a maintainer call on which contract the stanza handlers should rely on.', review: '2026-11-01' }, - { - id: 'get-history-msg-throws-instead-of-undefined', - target: 'pure:getHistoryMsg', - status: 'open', - // The generator now emits valid history-sync notifications, so the entry has - // to say which half of that it covers: only the *missing* one. Read - // structurally rather than by searching the serialised input — a - // notification nested under `deviceSentMessage` appears in the text but is - // not where either helper looks, and both correctly ignore it. - when: divergence => { - if (!isThrow(divergence.local) || divergence.upstream !== undefined) return false - const message = Array.isArray(divergence.input) ? divergence.input[0] : undefined - const protocol = (message as { protocolMessage?: Record } | undefined)?.protocolMessage - return protocol?.historySyncNotification === undefined - }, - reason: - 'Upstream returns `undefined` when the message carries no history-sync notification; baileyrs throws a Boom 400. Drop-in consumer code written as `const h = getHistoryMsg(msg); if (!h) return` therefore crashes against baileyrs. The fix is a signature change on a published API, so it belongs in its own commit rather than in the change that found it.', - review: '2026-11-01' - }, { id: 'clean-message-empty-jid-normalisation', target: /^pure:cleanMessage/u, diff --git a/src/__tests__/public-helpers-compatibility.test.ts b/src/__tests__/public-helpers-compatibility.test.ts index 550f8aea..440a5f75 100644 --- a/src/__tests__/public-helpers-compatibility.test.ts +++ b/src/__tests__/public-helpers-compatibility.test.ts @@ -290,7 +290,11 @@ describe('public helper compatibility', () => { it('extracts wrapped history notifications', () => { const notification = { syncType: proto.HistorySync.HistorySyncType.RECENT } assert.deepEqual(getHistoryMsg({ protocolMessage: { historySyncNotification: notification } }), notification) - assert.throws(() => getHistoryMsg({ conversation: 'not a history sync' }), /history sync notification/) + // Upstream returns undefined here. Throwing broke `const h = getHistoryMsg(m); if (!h) return`, + // which is the shape a drop-in caller writes. + assert.equal(getHistoryMsg({ conversation: 'not a history sync' }), undefined) + assert.equal(getHistoryMsg({}), undefined) + assert.equal(getHistoryMsg(undefined as never), undefined) }) it('matches message-device inference and bulk receipt grouping', () => {