c309fcce895231c6921bfc40db15c9ed7bc62476 — Sam Whited 3 months ago 741e8a5
roster: respond to IQs in roster push handler

Previously we didn't respond to the roster push IQs, meaning that the
library always responded with a default "unsupported feature" response.
Instead, respond with "success" if the push handler does not result in
an error, the error itself if it returns a stanza.Error, or just return
the error (terminating the stream) if another error is returned.
This last behavior may change in a future commit (eg. to send an
application specific error or an internal-server-error and keep the
stream running).

Signed-off-by: Sam Whited <sam@samwhited.com>
3 files changed, 65 insertions(+), 3 deletions(-)

M roster/roster.go
M roster/roster_test.go
@@ 30,6 30,8 @@ All notable changes to this project will be documented in this file.
- muc: new package implementing [XEP-0045: Multi-User Chat] and [XEP-0249: Direct MUC Invitations]
- roster: the roster `Iter` now returns the roster version being iterated over
  from the `Version` method
- roster: if a `stanza.Error` is returned from the `Push` handler it is now sent
  in response to the original roster push.
- stanza: implement [XEP-0203: Delayed Delivery]
- stanza: more general `UnmarshalError` function that doesn't focus on IQs
- stanza: add `Error` method to `Presence` and `Message`

@@ 45,6 47,8 @@ All notable changes to this project will be documented in this file.
  prevent data leaks across forms
- roster: the roster version is now always included, even if empty, to signal
  that we support roster versioning
- roster: the handler now responds with an IQ result if the roster `Push`
  handler does not return an error
- stanza: unmarshaling error IQs now works even if the error is not the first
  child in the payload
- styling: pre-block start tokens with no newline had nonsensical formatting

M roster/roster.go => roster/roster.go +15 -1
@@ 8,6 8,7 @@ package roster // import "mellium.im/xmpp/roster"
import (


@@ 29,6 30,8 @@ func Handle(h Handler) mux.Option {

// Handler responds to roster pushes.
// If Push returns a stanza.Error it is sent as an error response to the IQ
// push, otherwise it is passed through and returned from HandleIQ.
type Handler struct {
	Push func(ver string, item Item) error

@@ 47,7 50,18 @@ func (h Handler) HandleIQ(iq stanza.IQ, t xmlstream.TokenReadEncoder, start *xml
	return h.Push(ver, item)
	err = h.Push(ver, item)
	var stanzaErr stanza.Error
	isStanzaErr := errors.As(err, &stanzaErr)
	if isStanzaErr {
		_, err = xmlstream.Copy(t, iq.Error(stanzaErr))
		return err
	if err != nil {
		return err
	_, err = xmlstream.Copy(t, iq.Result(nil))
	return err

// Iter is an iterator over roster items.

M roster/roster_test.go => roster/roster_test.go +46 -2
@@ 165,8 165,52 @@ func TestReceivePush(t *testing.T) {

	out := b.String()
	if out != "" {
		t.Errorf("want=%q, got=%q", "", out)
	const expected = `<iq xmlns="jabber:client" type="result" from="juliet@example.com/chamber" id="a78b4q6ha463"></iq>`
	if out != expected {
		t.Errorf("wrong response: want=%q, got=%q", expected, out)

func TestReceivePushError(t *testing.T) {
	const itemJID = "nurse@example.com"
	const x = `<iq xmlns='jabber:client' id='a78b4q6ha463' to='juliet@example.com/chamber' type='set'><query xmlns='jabber:iq:roster' ver='testver'><item jid='` + itemJID + `'/></query></iq>`

	d := xml.NewDecoder(strings.NewReader(x))
	var b strings.Builder
	e := xml.NewEncoder(&b)

	h := roster.Handler{
		Push: func(ver string, item roster.Item) error {
			return stanza.Error{Condition: stanza.Forbidden}

	tok, err := d.Token()
	if err != nil {
		t.Errorf("unexpected error popping start token: %v", err)
	start := tok.(xml.StartElement)
	m := mux.New(roster.Handle(h))
	err = m.HandleXMPP(struct {
		TokenReader: d,
		Encoder:     e,
	}, &start)
	if err != nil {
		t.Errorf("unexpected error in handler: %v", err)
	err = e.Flush()
	if err != nil {
		t.Errorf("unexpected error flushing encoder: %v", err)

	out := b.String()

	const expected = `<iq xmlns="jabber:client" type="error" from="juliet@example.com/chamber" id="a78b4q6ha463"><error><forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></forbidden></error></iq>`
	if out != expected {
		t.Errorf("wrong response: want=%q, got=%q", expected, out)