Skip to content

event.Subscribe violates the b-2.xsd sequence and always sends its optional children #71

Description

@GyeongHoKim

event.Subscribe marshals to a payload that does not validate against the WS-BaseNotification schema it cites in its own doc comment (http://docs.oasis-open.org/wsn/b-2.xsd). There are two independent defects. Both are separate from the CreatePullPointSubscription namespace problem in #64, though the first one interacts with it.

For reference, b-2.xsd declares Subscribe as an xs:sequence:

# Child Cardinality Note
1 ConsumerReference [1][1]
2 Filter [0][1]
3 InitialTerminationTime [0][1] nillable="true"
4 SubscriptionPolicy [0][1]
5 <xs:any namespace="##other"> [0][n] trailing wildcard

The schema is elementFormDefault="qualified", so all four children are correctly wsnt:-prefixed today. The prefixes are not the problem here.

1. Children are emitted out of sequence order

event/operation.go declares SubscriptionPolicy before InitialTerminationTime, and encoding/xml emits fields in declaration order:

type Subscribe struct { //http://docs.oasis-open.org/wsn/b-2.xsd
	XMLName                struct{}                   `xml:"wsnt:Subscribe"`
	ConsumerReference      EndpointReferenceType      `xml:"wsnt:ConsumerReference"`
	Filter                 FilterType                 `xml:"wsnt:Filter"`
	SubscriptionPolicy     SubscriptionPolicy         `xml:"wsnt:SubscriptionPolicy"`
	InitialTerminationTime AbsoluteOrRelativeTimeType `xml:"wsnt:InitialTerminationTime"`
}

Reproduced bytes on current master, for a Subscribe carrying only a consumer reference and a termination time:

<wsnt:Subscribe><wsnt:ConsumerReference><Address>http://192.168.0.10:8080/notify</Address><ReferenceParameters></ReferenceParameters><Metadata></Metadata></wsnt:ConsumerReference><wsnt:Filter><wsnt:TopicExpression Dialect=""></wsnt:TopicExpression></wsnt:Filter><wsnt:SubscriptionPolicy ChangedOnly="false"></wsnt:SubscriptionPolicy><wsnt:InitialTerminationTime>PT60S</wsnt:InitialTerminationTime></wsnt:Subscribe>

The ordering is not cosmetic. Walking a validator through it:

  1. ConsumerReference and Filter match particles 1 and 2.
  2. Particle 3 is InitialTerminationTime, but SubscriptionPolicy appears instead. Particle 3 is optional, so it is skipped and SubscriptionPolicy matches particle 4.
  3. The only particle left is the wildcard, declared namespace="##other". The trailing wsnt:InitialTerminationTime is in the target namespace, which ##other explicitly excludes, so it cannot match.
  4. No particle remains. Validation fails.

So a schema-strict device rejects the request outright rather than silently ignoring one element.

On master the policy is a value type and is therefore always emitted, which means every Subscribe request is affected. #64 makes it an optional pointer, so after that lands the violation is latent and only surfaces when a caller sets SubscriptionPolicy. Either way the field order is wrong.

2. Optional children are always emitted

Filter and InitialTerminationTime are both minOccurs="0", but the Go fields are value types, so a zero-value Subscribe still ships both:

<wsnt:Subscribe><wsnt:ConsumerReference>…</wsnt:ConsumerReference><wsnt:Filter><wsnt:TopicExpression Dialect=""></wsnt:TopicExpression></wsnt:Filter><wsnt:InitialTerminationTime></wsnt:InitialTerminationTime></wsnt:Subscribe>
  • The empty wsnt:InitialTerminationTime is invalid on its own. Its type is wsnt:AbsoluteOrRelativeTimeType, a union of xs:dateTime and xs:duration, and "" belongs to neither lexical space. The element is nillable="true", so <wsnt:InitialTerminationTime xsi:nil="true"/> would be legal, but a bare empty element is not.
  • The wsnt:Filter is schema-valid. FilterType is <xs:any minOccurs="0" maxOccurs="unbounded"/> (b-2.xsd:57-61), so its namespace defaults to ##any and its processContents to strict (XSD 1.0 Part 1 §3.10.2), and wsnt:TopicExpression is globally declared at b-2.xsd:71, so the child resolves and validates. Dialect="" is valid too: xs:anyURI takes its lexical space from RFC 2396 (XSD 1.0 Part 2 §3.2.17.1), whose URI-reference production makes both parts optional, and §4.2 reads an empty reference as a same-document reference. The defect is therefore semantic rather than structural: an empty dialect URI is not resolvable by a device, and sending it defeats the "no Filter means notify all topics" semantics.

This is the same class of defect that #44 fixed for FilterType.MessageContent and that #64 fixes for the three CreatePullPointSubscription children.

Proposed fix

type Subscribe struct { //http://docs.oasis-open.org/wsn/b-2.xsd
	XMLName                struct{}                    `xml:"wsnt:Subscribe"`
	ConsumerReference      EndpointReferenceType       `xml:"wsnt:ConsumerReference"`
	Filter                 *FilterType                 `xml:"wsnt:Filter,omitempty"`
	InitialTerminationTime *AbsoluteOrRelativeTimeType `xml:"wsnt:InitialTerminationTime,omitempty"`
	SubscriptionPolicy     *SubscriptionPolicy         `xml:"wsnt:SubscriptionPolicy,omitempty"`
}

Reordering the fields is not a source-breaking change for keyed struct literals. Making Filter and InitialTerminationTime pointers is a public API break: callers must take addresses when setting them and handle nil when reading. There are no production callers inside this repository — sdk/event/Subscribe_auto.go forwards the struct unchanged — but downstream users are affected. This mirrors the break already proposed in #64, so the two changes should land in the same release to keep the migration to a single step for downstream users.

I have this implemented locally with marshaling regressions covering the sequence order and each optional child independently, and can open a PR once #64 is resolved, since it builds on the pointer change there. Happy to take it in whatever order suits you.

Not covered here

EndpointReferenceType marshals Address, ReferenceParameters, and Metadata without a prefix, while ws-addr.xsd is elementFormDefault="qualified" and expects wsa:-qualified children; the two optional ones are also always emitted. The wsa prefix is already declared in Xlmns. That type is shared by other packages, so it seems better as its own issue rather than folded in here.

Environment

AI disclosure

I used Claude Code (Opus 5) to cross-check event.Subscribe against b-2.xsd, to reproduce the marshaled bytes quoted above, and to draft this report. The local implementation and its regression tests were written the same way. I verified the schema references, the reproduced payloads, and the validator walkthrough myself, and I take responsibility for the conclusions.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions