fish2018/pansou · error

[ ] 创建搜索请求失败

Error message

[%s] 创建搜索请求失败: %w

What it means

searchImpl builds a GET request with http.NewRequestWithContext against p.baseURL + "/s/<escaped keyword>.html". When http.NewRequestWithContext returns a non-nil error (unparseable URL, invalid method, or a nil/already-cancelled context), the plugin wraps it with this message so the plugin name is preserved. It means the search never started — no network I/O occurred.

Solutions

  1. Fix the plugin's baseURL configuration so it is a valid absolute URL with a scheme, e.g. "https://zlx.example.com".
  2. url.PathEscape(keyword) can still leave characters net/url rejects in some positions; test the exact keyword against url.Parse and strip/replace offending characters before calling Search.
  3. Trim surrounding whitespace from the configured baseURL before storing it; verify the config file was loaded and the field is non-empty.

Example fix

// before
searchURL := fmt.Sprintf("%s/s/%s.html", strings.TrimRight(p.baseURL, "/"), url.PathEscape(keyword))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)

// after
base := strings.TrimSpace(p.baseURL)
if u, perr := url.Parse(base); perr != nil || u.Scheme == "" || u.Host == "" {
    return nil, fmt.Errorf("[%s] 无效的 baseURL: %q", p.Name(), base)
}
searchURL := fmt.Sprintf("%s/s/%s.html", strings.TrimRight(base, "/"), url.PathEscape(keyword))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(strings.TrimSpace(p.baseURL))
if err != nil || u.Scheme == "" || u.Host == "" {
    return fmt.Errorf("invalid baseURL %q: %v", p.baseURL, err)
}
if keyword == "" {
    return fmt.Errorf("keyword must be non-empty")
}

Type guard

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

Try / catch

results, err := plugin.Search(ctx, keyword)
if err != nil {
    var opErr *url.Error
    if errors.As(err, &opErr) && opErr.Op == "parse" {
        // config problem: do not retry, surface configuration error
        return fmt.Errorf("check plugin baseURL config: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling searchImpl (directly or via the plugin Search entry point) when the resulting searchURL cannot be parsed as a valid absolute URL — e.g. p.baseURL is empty, contains spaces/control characters, or has a malformed scheme — or when ctx is invalid.

Common situations: Misconfigured plugin baseURL in the app config (empty string, missing scheme like "zlx.example.com" instead of "https://zlx.example.com", trailing whitespace, or copy-pasted fullwidth characters). Also rare library/version regressions in net/http URL parsing.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07). Data as JSON: /api/errors/c696330ed9ad977f. Report an issue: GitHub.

Appendix: source

Thrown at plugin/zlxapp/zlxapp.go:104

	return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
}

func (p *ZlxappPlugin) searchImpl(client *http.Client, keyword string, _ map[string]interface{}) ([]model.SearchResult, error) {
	keyword = cleanText(keyword)
	if keyword == "" {
		return []model.SearchResult{}, nil
	}
	if client == nil {
		client = http.DefaultClient
	}

	ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
	defer cancel()

	searchURL := fmt.Sprintf("%s/s/%s.html", strings.TrimRight(p.baseURL, "/"), url.PathEscape(keyword))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
	if err != nil {
		return nil, fmt.Errorf("[%s] 创建搜索请求失败: %w", p.Name(), err)
	}
	setRequestHeaders(req, p.baseURL)

	resp, err := doRequestWithRetry(client, req)
	if err != nil {
		return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize+1))
	if err != nil {
		return nil, fmt.Errorf("[%s] 读取搜索响应失败: %w", p.Name(), err)
	}
	if len(body) > maxResponseSize {
		return nil, fmt.Errorf("[%s] 搜索响应超过 %d 字节", p.Name(), maxResponseSize)
	}

	items, err := parseListItems(body)

View on GitHub (pinned to beaa561337)