fish2018/pansou · error
[ ] 创建搜索请求失败
Error message
[%s] 创建搜索请求失败: %w
What it means
This error is returned by HaitunsouPlugin.searchImpl when http.NewRequestWithContext fails to construct the GET request against the plugin's configured base URL plus the URL-escaped search keyword. In practice http.NewRequestWithContext only fails when the target URL fails url.Parse (invalid control characters, malformed URL), so it almost always indicates a bad baseURL configuration or a keyword containing characters that survived PathEscape and produced an unparseable URL. The original parse error is wrapped with %w for errors.Is/As inspection.
Solutions
- Inspect the wrapped error with errors.As(*url.Error) to see the exact URL parse failure and fix the baseURL configuration value
- Ensure baseURL comes from a validated source; validate with url.Parse(p.baseURL) at plugin construction time
- Sanitize the keyword of control characters before searchImpl builds the URL
- Fall back to the defaultBaseURL if the configured one fails validation
Example fix
// before
searchURL := fmt.Sprintf("%s/s/%s.html", strings.TrimRight(p.baseURL, "/"), url.PathEscape(keyword))
// after
if _, err := url.Parse(p.baseURL); err != nil {
return nil, fmt.Errorf("[%s] invalid baseURL: %w", p.Name(), err)
}
searchURL := fmt.Sprintf("%s/s/%s.html", strings.TrimRight(p.baseURL, "/"), url.PathEscape(keyword)) Defensive patterns
Strategy: validation
Validate before calling
if _, err := url.Parse(strings.TrimSpace(p.baseURL)); err != nil {
return nil, fmt.Errorf("baseURL invalid: %w", err)
}
if strings.ContainsFunc(keyword, func(r rune) bool { return r < 0x20 }) {
return nil, errors.New("keyword contains control characters")
} Try / catch
if _, err := plugin.Search(kw, nil); err != nil {
var urlErr *url.Error
if errors.As(err, &urlErr) { /* bad URL: fix config, don't retry */ }
} Prevention
- Validate baseURL with url.Parse at plugin construction
- Never inject user input into a URL without PathEscape
- Reject control characters in keywords before search
- Pin the base URL to a tested constant unless config is validated
When it happens
Trigger: Calling Search/SearchWithResult when p.baseURL is malformed (e.g. configured with a space, control character, or invalid scheme such as 'htp:/') so that fmt.Sprintf produces an unparseable URL, or when the cleaned keyword contains raw control bytes that PathEscape leaves invalid for a URL.
Common situations: Deployments overriding the default https://www.haitunsou.com base URL via config with a typo, trailing garbage, or an env var containing whitespace; unusual keywords with embedded newline/control characters after cleanText.
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/399a4cdb95799ab5.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/haitunsou/haitunsou.go:91
func (p *HaitunsouPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
}
func (p *HaitunsouPlugin) 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 := client.Do(req)
if err != nil {
return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("[%s] 搜索请求返回 HTTP %d", p.Name(), resp.StatusCode)
}
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)
}View on GitHub (pinned to beaa561337)