diff --git a/httpconnection.go b/httpconnection.go index 46f84e6..f86cc5b 100644 --- a/httpconnection.go +++ b/httpconnection.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "net/url" - "path" - "nhooyr.io/websocket" + "path" ) // Doer is the *http.Client interface @@ -18,9 +18,10 @@ type Doer interface { } type httpConnection struct { - client Doer - headers func() http.Header - transports []TransportType + client Doer + headers func() http.Header + transports []TransportType + skipNegotiation bool } // WithHTTPClient sets the http client used to connect to the signalR server. @@ -55,6 +56,22 @@ func WithTransports(transports ...TransportType) func(*httpConnection) error { } } +func WithSkipNegotiation() func(*httpConnection) error { + return func(c *httpConnection) error { + c.skipNegotiation = true + return nil + } +} + +func isWebSocketsSupported(transport []TransportType) bool { + for _, t := range transport { + if t == TransportWebSockets { + return true + } + } + return false +} + // NewHTTPConnection creates a signalR HTTP Connection for usage with a Client. // ctx can be used to cancel the SignalR negotiation during the creation of the Connection // but not the Connection itself. @@ -81,6 +98,28 @@ func NewHTTPConnection(ctx context.Context, address string, options ...func(*htt return nil, err } + slog.Debug("Evaluate skipNegotiation viability", "isWebSocketsSupported", isWebSocketsSupported(httpConn.transports), "httpConn.skipNegotiation", httpConn.skipNegotiation) + canSkipNegotiation := httpConn.skipNegotiation && isWebSocketsSupported(httpConn.transports) + + var conn Connection + if !canSkipNegotiation { + conn, err = newHTTPConnectionNegotiation(reqURL, address, ctx, httpConn) + } else { + slog.Debug("Skipping WebSocket negotiation") + opts := &websocket.DialOptions{HTTPHeader: httpConn.headers()} + ws, _, err := websocket.Dial(ctx, reqURL.String(), opts) + if err != nil { + return nil, err + } + conn = newWebSocketConnection(context.Background(), "", ws) + } + + return conn, nil +} + +func newHTTPConnectionNegotiation(reqURL *url.URL, address string, ctx context.Context, httpConn *httpConnection) (Connection, error) { + slog.Info("Negotiate connection with server", "url", reqURL.String()) + negotiateURL := *reqURL negotiateURL.Path = path.Join(negotiateURL.Path, "negotiate") req, err := http.NewRequestWithContext(ctx, "POST", negotiateURL.String(), nil) @@ -123,7 +162,7 @@ func NewHTTPConnection(ctx context.Context, address string, options ...func(*htt reqURL.RawQuery = q.Encode() // Select the best connection - var conn Connection + var conn Connection = nil switch { case negotiateResponse.hasTransport("WebTransports"): // TODO @@ -180,7 +219,7 @@ func NewHTTPConnection(ctx context.Context, address string, options ...func(*htt } } - return conn, nil + return conn, err } // closeResponseBody reads a http response body to the end and closes it