Some Cancel/Remove messages go out with an empty <references/>, so consumers cannot tell which alert is being cancelled.
Example: message 125772 (Remove, TargetAreaCode 034FWFMAEDWINOLL) has empty references:
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
<script/>
<script/>
<identifier>125772</identifier>
<sender>www.gov.uk/environment-agency</sender>
<sent>2026-08-31T07:20:24+00:00</sent>
<status>Actual</status>
<msgType>Cancel</msgType>
<source>Flood warning service</source>
<scope>Public</scope>
<references/> <---------------- HERE
<info>
<language>en-GB</language>
<category>Met</category>
<event>
<![CDATA[ Remove ]]>
</event>
<responseType>AllClear</responseType>
<urgency>Immediate</urgency>
<severity>Minor</severity>
<certainty>Likely</certainty>
<expires>2026-09-01T07:20:24+00:00</expires>
<senderName>Environment Agency</senderName>
Other remove messages such as 125788, sent three minutes later, correctly references its original alert:
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
<script/>
<script/>
<identifier>125788</identifier>
<sender>www.gov.uk/environment-agency</sender>
<sent>2026-08-31T07:23:13+00:00</sent>
<status>Actual</status>
<msgType>Cancel</msgType>
<source>Flood warning service</source>
<scope>Public</scope>
<references>www.gov.uk/environment-agency,125764,2026-08-30T23:25:16+00:00</references>
<info>
<language>en-GB</language>
<category>Met</category>
<event>
<![CDATA[ Remove ]]>
</event>
<responseType>AllClear</responseType>
<urgency>Immediate</urgency>
<severity>Minor</severity>
<certainty>Likely</certainty>
<expires>2026-09-01T07:23:13+00:00</expires>
<senderName>Environment Agency</senderName>
Root cause: in lib/functions/processMessage.js, buildReference only writes the reference when the prior message is still live (i.e. has not expired):
if (lastMessage && lastMessage.expires > new Date()) { ... }
return ''
That guard is sensible for chaining Alert to Update, but it is also applied to Cancels. When the warning being withdrawn has already expired, the reference is dropped and the Cancel goes out with nothing identifying what it cancels.
Impact: an empty-references Cancel passes the base CAP v1.2 schema but is semantically incomplete, and it fails the common CAP profiles that require references on Update and Cancel. Consumers that key on references cannot action the withdrawal, so a cancelled warning can remain active downstream.
Proposed fix: for msgType === 'Cancel', always write the reference to the prior message (drop the expires > new Date() check on the cancel path), so a Remove always identifies the alert it withdraws.
Some Cancel/Remove messages go out with an empty
<references/>, so consumers cannot tell which alert is being cancelled.Example: message 125772 (Remove, TargetAreaCode
034FWFMAEDWINOLL) has empty references:Other remove messages such as 125788, sent three minutes later, correctly references its original alert:
Root cause: in
lib/functions/processMessage.js,buildReferenceonly writes the reference when the prior message is still live (i.e. has not expired):That guard is sensible for chaining Alert to Update, but it is also applied to Cancels. When the warning being withdrawn has already expired, the reference is dropped and the Cancel goes out with nothing identifying what it cancels.
Impact: an empty-references Cancel passes the base CAP v1.2 schema but is semantically incomplete, and it fails the common CAP profiles that require references on Update and Cancel. Consumers that key on references cannot action the withdrawal, so a cancelled warning can remain active downstream.
Proposed fix: for
msgType === 'Cancel', always write the reference to the prior message (drop theexpires > new Date()check on the cancel path), so a Remove always identifies the alert it withdraws.