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

  1. Check that a valid SHODAN_API_KEY is set in the environment or provider config.
  2. Verify the key has access to Shodan's DNS/subdomains endpoint (requires a paid plan or credits).
  3. 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

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 {
					return

View on GitHub (pinned to 7a0b91f0fa)