projectdiscovery/subfinder · error
%v
Error message
%v
What it means
The shodan source forwards the API's own error string verbatim (%v of response.Error) as an Error Result. Shodan returns an "error" field in its JSON whenever the request is rejected — invalid key, rate limit, or bad query — and the library surfaces that message directly to the caller.
Solutions
- Check that a valid SHODAN_API_KEY is set in the environment or provider config.
- Verify the key has access to Shodan's DNS/subdomains endpoint (requires a paid plan or credits).
- Slow down or retry later if the message indicates rate limiting.
Example fix
// before: key missing export PATH=... // after: export valid key export SHODAN_API_KEY=xxxxxxxxxxxxxxxx
Defensive patterns
Strategy: try-catch
Validate before calling
if os.Getenv("SHODAN_API_KEY") == "" { return errors.New("SHODAN_API_KEY not set") } Try / catch
for r := range results {
if r.Type == subscraping.Error {
if strings.Contains(r.Error.Error(), "invalid API key") {
// fix key and re-run
} else if strings.Contains(r.Error.Error(), "rate") {
// backoff and retry
}
}
} Prevention
- Validate the Shodan key before starting a scan (hit a cheap endpoint once).
- Respect Shodan's rate limits (max ~1 req/sec on free plans).
- Monitor API credit usage; Shodan DNS access requires a paid plan.
When it happens
Trigger: The Shodan API responds with JSON whose Error field is non-empty, e.g. "invalid API key", "403 Forbidden: insufficient credits", or rate-limit messages from https://api.shodan.io during enumeration.
Common situations: Missing or expired SHODAN_API_KEY; free-tier key lacking access to the subdomain/DNS endpoints; hitting Shodan's per-second or monthly credit limits.
Related errors
AI-assisted analysis of projectdiscovery/subfinder@7a0b91f0fa (2026-09-06).
Data as JSON: /api/errors/749bff76763475cd.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/subscraping/sources/shodan/shodan.go:85
results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: err}
s.errors++
session.DiscardHTTPResponse(resp)
return
}
defer session.DiscardHTTPResponse(resp)
var response dnsdbLookupResponse
err = jsoniter.NewDecoder(resp.Body).Decode(&response)
if err != nil {
results <- subscraping.Result{Source: s.Name(), Type: subscraping.Error, Error: err}
s.errors++
return
}
if response.Error != "" {
results <- subscraping.Result{
Source: s.Name(), Type: subscraping.Error, Error: fmt.Errorf("%v", response.Error),
}
s.errors++
return
}
for _, data := range response.Subdomains {
select {
case <-ctx.Done():
return
default:
}
value := fmt.Sprintf("%s.%s", data, response.Domain)
results <- subscraping.Result{
Source: s.Name(), Type: subscraping.Subdomain, Value: value,
}
s.results++
if maxResults > 0 && s.results >= maxResults {
returnView on GitHub (pinned to 7a0b91f0fa)