siyuan-note/siyuan · error

URL must start with http:// or https://

Error message

URL must start with http:// or https://

What it means

resolvePublicTarget normalizes the outbound request target for the SSRF-safe dialer: it validates the URL scheme and host. Only http (port 80) and https (port 443) are supported; any other scheme is rejected before any connection is made, because the safe dialer can only reason about plain TCP targets.

Solutions

  1. Prefix the target with https:// (or http:// explicitly)
  2. Validate the scheme in the calling code before invoking the agent request tool
  3. Never feed file://, ftp://, or ws:// URLs to the HTTP request path — use the appropriate subsystem instead
  4. Sanitize model-generated URLs to guarantee a valid absolute http(s) URL

Example fix

// before
url := "example.com/api"
// after
url := "https://example.com/api"
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(target)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
    return fmt.Errorf("target must be an absolute http(s) URL: %q", target)
}

Try / catch

if strings.Contains(err.Error(), "must start with http:// or https://") {
    // normalize the URL with a scheme and retry once
}

Prevention

When it happens

Trigger: RoundTrip receives a request whose URL scheme is not http/https — e.g. ws://, ftp://, file://, or a URL without any scheme — passed to the SSRF-safe HTTP client used by agent tools.

Common situations: An AI agent constructing a URL from user/injected input with a missing or exotic scheme; a developer passing a relative path or custom scheme to the fetch helper; template-built URLs losing the https:// prefix.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/2c4003f237009bdc. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/httprequest.go:153

		conn.Close()
		return nil, err
	}
	resp.Request = req
	resp.Body = newConnectionReadCloser(req.Context(), resp.Body, conn)
	return resp, nil
}

func (t *ssrfSafeTransport) resolvePublicTarget(ctx context.Context, targetURL *url.URL) (string, error) {
	host := targetURL.Hostname()
	port := targetURL.Port()
	if port == "" {
		switch targetURL.Scheme {
		case "http":
			port = "80"
		case "https":
			port = "443"
		default:
			return "", errors.New("URL must start with http:// or https://")
		}
	}

	if ip := net.ParseIP(host); ip != nil {
		if isPrivateIP(ip) {
			return "", errors.New("access to private/internal IP is prohibited")
		}
		return net.JoinHostPort(ip.String(), port), nil
	}

	ips, err := t.lookupIPAddr(ctx, host)
	if err != nil {
		return "", errors.New("failed to resolve host: " + err.Error())
	}
	if len(ips) == 0 {
		return "", errors.New("host has no IP address: " + host)
	}
	for _, ipAddr := range ips {

View on GitHub (pinned to 9f775e8a12)