MHSanaei/3x-ui · warning

unsupported URL scheme %q

Error message

unsupported URL scheme %q

What it means

SanitizeHTTPURL rejects any URL whose scheme is not exactly http or https after url.Parse. This is the first gate of the panel's SSRF-hardened URL intake (used for things like URL-test/health-check endpoints): non-HTTP schemes are refused before any DNS or request happens, because schemes like file://, gopher://, or unix-socket-style URLs have no safe outbound semantics here.

Source

Thrown at internal/web/service/url_safety.go:26

	"strings"
	"time"

	"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
)

// SanitizeHTTPURL validates and normalizes an http(s) URL without resolving
// DNS. Use SanitizePublicHTTPURL at the point of an outbound request.
func SanitizeHTTPURL(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "", nil
	}
	u, err := url.Parse(raw)
	if err != nil {
		return "", err
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return "", fmt.Errorf("unsupported URL scheme %q", u.Scheme)
	}
	if u.Host == "" || u.Hostname() == "" {
		return "", fmt.Errorf("URL host is required")
	}
	clean := &url.URL{
		Scheme:   u.Scheme,
		Host:     u.Host,
		Path:     u.Path,
		RawPath:  u.RawPath,
		RawQuery: u.RawQuery,
		Fragment: u.Fragment,
	}
	return clean.String(), nil
}

// SanitizePublicHTTPURL validates and normalizes an http(s) URL, then blocks
// private/internal targets unless the caller explicitly allows them.
func SanitizePublicHTTPURL(raw string, allowPrivate bool) (string, error) {

View on GitHub (pinned to ad32144c42)

Solutions

  1. Prefix the input with http:// or https:// before storing it (most common fix for user-entered hosts).
  2. Validate the field at the UI/API layer with a scheme allowlist so the user gets feedback before save.
  3. If the target legitimately needs another scheme, it does not belong in an HTTP-checked field — route it through a different, purpose-built config path.

Example fix

// before
raw := "example.com/health"
clean, err := service.SanitizeHTTPURL(raw) // unsupported URL scheme ""

// after
raw := "example.com/health"
if !strings.Contains(raw, "://") {
    raw = "https://" + raw
}
clean, err := service.SanitizeHTTPURL(raw)
Defensive patterns

Strategy: validation

Validate before calling

// Enforce scheme before storing/using a URL
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
    if !strings.Contains(raw, "://") && raw != "" {
        raw = "https://" + raw // auto-upgrade bare hosts
    }
}

Type guard

func isHTTPURL(raw string) bool {
    u, err := url.Parse(strings.TrimSpace(raw))
    return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
}

Try / catch

clean, err := service.SanitizeHTTPURL(raw)
if err != nil {
    if strings.Contains(err.Error(), "unsupported URL scheme") {
        // fix input to http(s), never broaden the allowlist
    }
}

Prevention

When it happens

Trigger: Passing 'ftp://host/file', 'file:///etc/passwd', 'javascript:...', a scheme with wrong case is fine (url.Parse lowercases), but 'HTTPS://x' with embedded whitespace or a URL like 'example.com/path' (no scheme at all, so u.Scheme == "" and it fails here rather than at the host check) will trigger it. Note: a schemeless input does NOT hit this branch only if it parses with empty scheme — empty scheme is also 'unsupported'.

Common situations: Users pasting a bare domain into a field that expects a full URL; config imports carrying legacy gopher/socks URLs; typo'd schemes like 'http//host'.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/6767e829c6f6bb62. Report an issue: GitHub.