Skip to content

OBSERVE registration failures are invisible to the caller #55

Description

@mbillow

A consumer that subscribes to many hrefs on one session needs to tell "this registration never took" from "this resource just hasn't changed yet". Right now it can't: subscribe() returns as soon as the register CON is written, and the only per-href signal that ever reaches the caller is on_notification. Both of the wire-level signals that would answer the question are dropped inside _dispatch_coap.

1. A non-2.05 on an observe token is logged and swallowed

dtls_session.py:697

if code != 0x45:
    logger.warning("observe %s: non-2.05 %s", href, fmt_code(code))
    return

That's a definitive "this registration failed", and it's the strongest
evidence available anywhere in the stack — but it goes to the log and
never to the caller, so acting on it means scraping log lines.

2. The Observe option is never read from a response

Option 6 is only ever written — register at dtls_session.py:1100,
deregister at :465. Nothing inspects it on the way back in; the
notification path reads only BLOCK2 out of ropts.

Two consequences:

  • A declined registration is indistinguishable from a successful one.
    RFC 7641 §4.1: a server that does not add the client to its observer
    list still answers the GET, just without the Observe option. Since any
    2.05 on that token is treated as a notification, the caller records the
    href as push-covered and stops polling something that will never push
    again. This is the dangerous direction — it fails toward stale data.
  • The initial registration response can't be told from a state change.
    The option's value is a sequence number (§3.4). Without it, "this href
    notified" means "it said something once", not "it pushes changes" —
    and every healthy href says something once, at registration. A caller
    wanting the stronger property has no way to compute it.

Why silence isn't a workable substitute

Inferring from "never notified" conflates at least three causes:

  • registration refused or errored (cases 1 and 2 above),
  • the register CON was lost — subscribe() is fire-and-forget and has no
    retransmit (unlike _blockwise_get's _BLOCK_MAX_ATTEMPTS), so a
    dropped datagram means no response ever arrives,
  • a large resource whose initial representation is legitimately deferred:
    _dispatch_coap withholds any notification with Block2 M=1 or NUM>0 and
    hands it to the refetch worker, which is serialized, paced at 5 req/s
    and bounded by _REFETCH_TIMEOUT_S = 15.0.

Only the third recovers on its own, and the caller can't tell which it is.

Suggested shape

Matching the existing self.on_notification = on_notification # fn(href, payload_bytes)
attribute at :251:

  • on_observe_error = None # fn(href, code) — invoked where the bare
    warning is today.
  • Something that surfaces registration acceptance and the sequence number.
    Either an extra argument on the notification callback or a separate
    on_observe_registered(href, accepted: bool); the useful part is
    distinguishing "2.05 with no Observe option" and exposing the seq, so
    the exact shape is yours to pick.

Both default to None, so current behavior is unchanged for existing
callers.

Happy to send a PR if the direction looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions