sipeed/picoclaw · error

invalid proxy URL %q: %w

Error message

invalid proxy URL %q: %w

What it means

NewApiClient for the Weixin iLink API rejects a configured proxy string because url.Parse fails on it. Go's url.Parse almost never errors — it fails on control characters, an embedded NUL, or a malformed percent-escape — so this fires only for genuinely malformed proxy strings, not for a wrong-but-well-formed host. The client refuses to build rather than silently ignoring the proxy.

Source

Thrown at pkg/channels/weixin/api.go:43

type ApiClient struct {
	BaseURL    string
	Token      string
	HttpClient *http.Client
}

func NewApiClient(baseURL, token string, proxy string) (*ApiClient, error) {
	if baseURL == "" {
		baseURL = "https://ilinkai.weixin.qq.com/"
	}

	client := &http.Client{
		// Default timeout; will be overridden per context
	}

	if proxy != "" {
		proxyURL, err := url.Parse(proxy)
		if err != nil {
			return nil, fmt.Errorf("invalid proxy URL %q: %w", proxy, err)
		}

		// Clone the default transport so we preserve all default settings (TLS, HTTP/2, timeouts, keep-alives)
		if defaultTransport, ok := http.DefaultTransport.(*http.Transport); ok {
			transport := defaultTransport.Clone()
			transport.Proxy = http.ProxyURL(proxyURL)
			client.Transport = transport
		} else {
			// Fallback: preserve previous behavior if DefaultTransport is not the expected type
			client.Transport = &http.Transport{
				Proxy: http.ProxyURL(proxyURL),
			}
		}
	}

	return &ApiClient{
		BaseURL:    baseURL,
		Token:      token,

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Print the proxy string with %q to expose hidden control characters/newlines, then strip or fix them
  2. Percent-encode credentials in the proxy URL correctly (a literal % must be %25)
  3. Validate the proxy URL early in config loading with url.Parse and fail with a clear message at startup
  4. If no proxy is intended, leave the setting empty instead of a placeholder like "none"

Example fix

# before
proxy: "http://user:pa%ss@proxy.local:8080"  # invalid % escape -> error 666

# after
proxy: "http://user:pa%25ss@proxy.local:8080"  # % encoded as %25
Defensive patterns

Strategy: validation

Validate before calling

func validateProxy(proxy string) error {
    if proxy == "" {
        return nil
    }
    u, err := url.Parse(proxy)
    if err != nil {
        return fmt.Errorf("proxy %q unparseable: %w", proxy, err)
    }
    if u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "socks5" {
        return fmt.Errorf("proxy scheme %q unsupported", u.Scheme)
    }
    return nil
}

if err := validateProxy(cfg.Proxy); err != nil { return err }

Type guard

func isInvalidProxyURL(err error) bool {
    return err != nil && strings.Contains(err.Error(), "invalid proxy URL")
}

Try / catch

client, err := weixin.NewApiClient(baseURL, token, proxy)
if err != nil {
    if isInvalidProxyURL(err) {
        // config bug: fix/strip the proxy string, never retry
    }
}

Prevention

When it happens

Trigger: Constructing the Weixin API client with a non-empty proxy setting containing control characters, a raw newline/NUL, or an invalid percent-encoding (e.g. "http://proxy:80%zz" or a value with trailing CR from a config file). Well-formed-but-unreachable proxies do NOT trigger this; they fail later at request time.

Common situations: A .env or YAML value with an unescaped newline or trailing \r (Windows line endings); a percent sign in the proxy password that is not double-encoded; copy-pasting a proxy URL with invisible characters; a DNS name with a stray control byte.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/20cfaf003ab3af6d. Report an issue: GitHub.