Skip to content

ConnectFn: request broadcasts are dropped before subscription #161

Description

@linkdata

Summary

Request-targeted broadcasts issued from a successful ConnectFn are silently dropped. This affects Request.Redirect, Request.Alert, reloads, JavaScript calls, and other helpers built on Jaws.Broadcast.

Class / severity

Correctness / lifecycle — Medium. Connection-initialization actions disappear without error on an otherwise healthy WebSocket.

Contract and valid usage

ConnectFn says it can interact with a Request before message processing starts. Request.Redirect imposes only the Jaws.Broadcast processing-loop requirement, and Request.ServeHTTP itself requires that loop to be running.

The callback below is synchronous, uses only its supplied Request, returns normally, and sends a valid relative redirect while Jaws.Serve is running.

Reproduction

func TestConnectFnRedirectDelivered(t *testing.T) {
	jw, err := jaws.New()
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(jw.Close)
	go jw.Serve()

	srv := httptest.NewServer(jw)
	t.Cleanup(srv.Close)

	initial := httptest.NewRequest(http.MethodGet, srv.URL+"/", nil)
	initial.RemoteAddr = "127.0.0.1:1"
	rq := jw.NewRequest(initial)

	called := make(chan struct{})
	rq.SetConnectFn(func(rq *jaws.Request) error {
		rq.Redirect("/from-connect-fn")
		close(called)
		return nil
	})

	hdr := http.Header{}
	hdr.Set("Origin", srv.URL)
	ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second)
	defer cancel()

	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") +
		"/jaws/" + rq.JawsKeyString()
	conn, _, err := websocket.Dial(ctx, wsURL,
		&websocket.DialOptions{HTTPHeader: hdr})
	if err != nil {
		t.Fatal(err)
	}
	defer conn.CloseNow()
	<-called

	// Repeated markers avoid assuming exactly when subscription finishes.
	stop := make(chan struct{})
	defer close(stop)
	go func() {
		ticker := time.NewTicker(10 * time.Millisecond)
		defer ticker.Stop()
		for {
			select {
			case <-stop:
				return
			case <-ticker.C:
				jw.Alert("info", "post-connect-marker")
			}
		}
	}()

	_, data, err := conn.Read(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Contains(data, []byte("Redirect")) {
		t.Fatalf("ConnectFn redirect was lost; first healthy frame: %s", data)
	}
}

The observed first frame is the post-connect Alert marker; it contains no Redirect.

Root cause

Request.ServeHTTP calls rq.onConnect() before rq.Jaws.subscribe(...). Redirect synchronously reaches the Serve loop, which has no subscription for this Request and therefore drops the key-targeted message.

Impact

Login/navigation initialization, connection notifications, or session-driven updates initiated by ConnectFn silently fail for the connecting page.

Suggested fix

Establish a buffered subscription before calling onConnect, and unsubscribe it if the callback fails. Alternatively, queue callback-time Request broadcasts until the subscription is installed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggoPull requests that update go code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions