Critical
- Renew busy-loop on persistent failure: after a failed
renewPullPoint, the loop re-read the (now in-past)
GrantedTermination and nextRenewInterval floored to 1s, hammering
the camera at 1 Hz until reconnect. nextRenewIntervalAfterError
decouples the failure path from the stale grant and backs off
at opts.RetryBackoff. renewLoop also routes through s.now() so
test clocks can drive it deterministically.
- Lost-update race on GrantedTermination: a renew result for an
old subscription could overwrite the grant on a new one if
attemptRecreate swapped pullPoint mid-flight. A generation
counter on Stream tracks subscription rotation; the renew loop
captures the generation before the SOAP call and discards the
result if the subscription was rotated.
- Credential leak when <Security> straddled the 64 KiB error cap:
the non-greedy regex required a closing tag and missed the
truncated case. wsseSecurityRE now matches close-tag-or-EOF.
Belt-and-braces wssePasswordRE redacts <Password> elements
outside any Security wrapper.
- addHeaderChildren accepted well-formed-but-element-free input
and produced a header-less request. Now errors out.
Important
- Wrapper detection in buildRefParamsHeader was HasSuffix-based and
misfired on children named *ReferenceParameters. Replaced with the
unambiguous wrapper-only contract: input must be the full
<*:ReferenceParameters> element returned by extractReferenceParameters.
- Renamed SoapOption → SendSoapOption and WithHeader → WithSOAPHeader
to disambiguate at call sites.
- Renamed WithHeader's `xml` parameter to headerContent to avoid
shadowing the encoding/xml package name.
- Documented terminationTimeRE's "first match only" semantics so
nobody re-uses it from PullMessages context where multiple
TerminationTime elements appear.
- goleak is now a direct require (go mod tidy).
Performance
- Added gosoap.AddStringHeaderContents (plural) for multi-root
header content. AddStringHeaderContent remains as-is for
backwards compatibility with external consumers. Device.go's
addHeaderChildren workaround is gone — one etree parse per
SendSoapWithOptions call instead of two.
Migration
- Device.go's own CallOnvifFunction and the three examples now
call SendSoapWithOptions, modelling the canonical path.
Docs / tests
- Trust-boundary warning on SendSoapWithHeader/Options godoc.
- Comments on createPullPointResp/Alt explain why TerminationTime
is intentionally omitted (renew timing fixtures).
- New tests: digest retry strips WS-Security, duplicate
WithSOAPHeader is last-wins, malformed TerminationTime yields
zero, margin==base falls to base/2, empty SubscriptionReference
yields empty ref params, Security straddling cap is redacted,
bare Password redacted, wrapper-only contract is enforced.
Two findings from the resource/security and API reviews of the
streamtest example.
Credentials
-----------
* loadPassword resolves the camera password in order:
1. ONVIF_PASSWORD environment variable (recommended).
2. -password-file <path> (newline trimmed).
3. Interactive prompt when nothing else is set.
* -password flag still works but now logs a WARNING that the value
leaks into shell history and process listings. Documented as
'INSECURE' in the flag help.
* Updated package godoc with a Credentials section.
Errors-arm bug
--------------
* Previous code: case e := <-s.Errors() with no ok check. When the
Stream closed, this arm would spin on a closed channel printing
'<nil>' forever (until ctx-done elsewhere unblocked it). Mirror
the Events arm's ok pattern.
* Switched the error-log branch to inspect the typed errors added
in the previous commit: ErrRecreateFailed gets a louder 'camera
may be offline' log line; ErrPullFailed is a quieter
'will retry' since the loop handles transient pull errors
automatically.
Also prints '[after-reconnect]' on events carrying that flag so the
operator can see when the stream silently recovered a dropped
subscription — confirms the new observability surface is useful at
the CLI level.
API-shape changes flagged as 'hard to reverse after v1' by the
architect reviewer. Acceptable to do now while no external code
imports the package; would be breaking later.
Surface tightening
------------------
* Decode unexported to decode. The Stream is the only intended caller;
exposing the helper invited future API drift. Same-package tests
still reach it.
* TopicFilter renamed to RawTopicFilter to signal that the value is
fed verbatim into the SOAP envelope and is the 'advanced escape
hatch', not the supported routing surface. Callers should normally
leave it empty and rely on Classify.
Options zero-value policy clarified
-----------------------------------
* Field godoc on every numeric option now explicitly states 'zero
means default' so the policy is local, not buried in
withDefaults().
* New DisableReconnect bool — addresses the
ReconnectAfterFailures=0-as-disable footgun the API reviewer flagged.
Reader can no longer confuse 'unset, fallback to default' with 'opt
out of reconnect'.
* BufferSize semantics extended: zero -> default (16), negative ->
unbuffered (0), positive -> explicit size. Lets callers ask for
back-pressure-only channels.
Default tuning
--------------
* MessageLimit default raised from 10 to 32. Busy AXIS cameras with
several configured inputs / analytics rules can burst beyond 10
per pull; the lower cap meant up to one PullTimeout of added
latency for the queued overflow without saving anything
meaningful. 32 covers observed bursts with no real overhead on
quiet pulls.
Caller interface
----------------
* Doc comment now states the goroutine-safety contract Stream
depends on (pull loop and renew loop call from separate
goroutines). *onvif.Device satisfies it via http.Client.
Package documentation
---------------------
* doc.go rewritten as a real godoc landing page: usage snippet,
invariants (channel close, Close idempotency, NewStream does I/O,
buffer semantics), reconnect behaviour and AfterReconnect, and a
pointer to topics.go for the classifier table. Replaces the
earlier stub that referenced unimplemented identifiers.
Adds a small command at examples/event/stream that opens a real ONVIF
event stream against a camera and prints decoded events one per line.
Intended for verifying the classifier against actual hardware (AXIS in
particular) and as runnable documentation for new consumers of the
package — point it at a configured camera, trigger motion, watch the
events arrive.
Behaviour
---------
* Required flags: -xaddr, -username, -password (matches existing
examples/event/* commands so anyone running the older subscribe /
pullmessage demos already knows the shape).
* Optional -filter passes through to Options.TopicFilter; default
empty so AXIS works out of the box.
* -duration N stops after N (default 0 = run until Ctrl-C).
* Prints kind/state/op/topic on each event plus source and data maps
when present, so multi-item ONVIF payloads (AXIS AOA
active+classType+confidence, DigitalInput InputToken+LogicalState)
are visible without re-reading PullMessages SOAP.
* Errors channel surfaced to stderr via log; the stream auto-recovers
per the reconnect logic in stream.go so transient errors do not
terminate the demo.
Not part of any CI; not a production tool — this is a verification
harness.