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
- Fix the plugin's baseURL configuration so it is a valid absolute URL with a scheme, e.g. "https://zlx.example.com".
- 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.
- 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
- Validate baseURL at plugin init/startup, not per request
- Trim whitespace and strip invisible/fullwidth characters from config values at load time
- Add a startup smoke test that calls url.Parse on every configured endpoint
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)