gorilla/websocket · error

malformed ws or wss URL

Error message

malformed ws or wss URL

What it means

errMalformedURL is returned by DialContext when the URL string passed to Dial/DialContext cannot be parsed or does not use the ws:// or wss:// scheme. DialContext parses the URL with url.Parse and then validates the scheme before connecting.

Source

Thrown at client.go:135

	// EnableCompression specifies if the client should attempt to negotiate
	// per message compression (RFC 7692). Setting this value to true does not
	// guarantee that compression will be supported. Currently only "no context
	// takeover" modes are supported.
	EnableCompression bool

	// Jar specifies the cookie jar.
	// If Jar is nil, cookies are not sent in requests and ignored
	// in responses.
	Jar http.CookieJar
}

// Dial creates a new client connection by calling DialContext with a background context.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
	return d.DialContext(context.Background(), urlStr, requestHeader)
}

var errMalformedURL = errors.New("malformed ws or wss URL")

func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
	hostPort = u.Host
	hostNoPort = u.Host
	if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
		hostNoPort = hostNoPort[:i]
	} else {
		switch u.Scheme {
		case "wss":
			hostPort += ":443"
		case "https":
			hostPort += ":443"
		default:
			hostPort += ":80"
		}
	}
	return hostPort, hostNoPort
}

View on GitHub (pinned to e064f32e36)

Solutions

  1. Ensure the URL starts with ws:// or wss:// and parses with url.Parse
  2. Prefer ws+tls-free ws:// for plaintext and wss:// for TLS; never use http(s) scheme strings
  3. Validate the scheme in config-loading code before constructing the dialer
  4. Use url.Parse yourself and check err and u.Scheme to surface a clearer message

Example fix

// before
u := os.Getenv("WS_URL") // e.g. "example.com/ws"
conn, _, err := dialer.Dial(u, nil)
// after
u := os.Getenv("WS_URL")
if !strings.HasPrefix(u, "ws://") && !strings.HasPrefix(u, "wss://") {
    u = "wss://" + u
}
conn, _, err := dialer.Dial(u, nil)
Defensive patterns

Strategy: validation

Validate before calling

func validateWSURL(raw string) error {
    u, err := url.Parse(raw)
    if err != nil {
        return err
    }
    if u.Scheme != "ws" && u.Scheme != "wss" {
        return fmt.Errorf("scheme must be ws or wss, got %q", u.Scheme)
    }
    if u.Host == "" {
        return errors.New("missing host")
    }
    return nil
}

Type guard

func isWSURL(raw string) bool {
    u, err := url.Parse(raw)
    return err == nil && (u.Scheme == "ws" || u.Scheme == "wss") && u.Host != ""
}

Try / catch

if err := validateWSURL(cfg.WSURL); err != nil {
    return fmt.Errorf("config error: %w", err)
}
conn, _, err := dialer.DialContext(ctx, cfg.WSURL, nil)
if err != nil {
    return err
}

Prevention

When it happens

Trigger: Passing a URL string to Dialer.Dial/DialContext that fails url.Parse, or one whose scheme is not "ws" or "wss" (e.g. http://, https://, or no scheme at all).

Common situations: Building the URL with the http scheme by habit, forgetting the scheme when concatenating host strings, environment variables containing bare hostnames ("example.com/socket" instead of "wss://example.com/socket"), or typos like ws:/ or wss//.

Understand the failure class

Related errors


AI-assisted analysis of gorilla/websocket@e064f32e36 (2026-08-31). Data as JSON: /api/errors/8a81bda25b51fa45. Report an issue: GitHub.