How to Handle API Key Errors and Authentication Failures in SpiderFoot Modules
SpiderFoot modules use a standardized error-handling pattern: verify the API key in handleEvent(), set self.errorState = True on any authentication failure, and abort further processing to prevent redundant requests.
Every SpiderFoot module inherits from SpiderFootPlugin and implements the same defensive checks for missing or invalid credentials. This guide walks through the exact implementation patterns found in the smicallef/spiderfoot source code, including concrete examples from production modules like sfp_virustotal and sfp_xforce.
The Core Architecture: errorState and Early Returns
SpiderFoot's plugin system revolves around a simple boolean flag: errorState. Defined in the base SpiderFootPlugin class ([spiderfoot/plugin.py](https://github.com/smicallef/spiderfoot/blob/master/spiderfoot/plugin.py)), this flag acts as a circuit breaker. Once set to True, the module stops all subsequent API requests for the duration of the scan.
Every handleEvent() method begins with this guard:
def handleEvent(self, event):
if self.errorState:
return # skip processing after a fatal error
This pattern prevents a cascade of failing requests when authentication has already failed.
Detecting Missing API Keys Before Requesting
The first line of defense occurs before any network call. Modules check their opts dictionary—which stores configuration including api_key—and fail fast if credentials are absent.
Pattern: Empty Key Check
In [modules/sfp_virustotal.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_virustotal.py#L184-L186), lines 184-186 implement this check:
if self.opts["api_key"] == "":
self.error(
f"You enabled {self.__class__.__name__} but did not set an API key!"
)
self.errorState = True
return
The [sfp_xforce.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_xforce.py#L215-L216) module extends this pattern for multi-field authentication, checking both api_key and api_password at lines 215-216:
if self.opts['api_key'] == "" or self.opts['api_password'] == "":
self.error("You enabled sfp_xforce but did not set an API key/password!")
return
Reusable Implementation Template
def handleEvent(self, event):
# Circuit breaker
if self.errorState:
return
# Validate required credential
if not self.opts.get("api_key"):
self.error(
f"You enabled {self.__class__.__name__} but did not set an API key!"
)
self.errorState = True
return
# Proceed with API calls...
Handling Authentication Failures After API Requests
Even with a key present, APIs reject invalid credentials or throttle excessive requests. SpiderFoot modules inspect HTTP response codes and JSON error payloads to detect these conditions.
Throttling: HTTP 204 Handling
VirusTotal rate-limits free tier requests. In [modules/sfp_virustotal.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_virustotal.py#L151-L154), lines 151-154 detect the throttling response:
if res['code'] == "204":
self.error("You are being rate-limited by VirusTotal.")
self.errorState = True
return None
Generic HTTP Authentication Errors
For standard 401/403 responses, modules follow this pattern:
def queryEndpoint(self, query):
res = self.sf.fetchUrl(
f"https://api.example.com/search?query={query}",
headers={"Authorization": f"Bearer {self.opts['api_key']}"},
timeout=self.opts['_fetchtimeout'],
useragent=self.opts['_useragent']
)
# Authentication failure detection
if res['code'] in ("401", "403"):
self.error("Invalid API credentials — authentication failed.")
self.errorState = True
return None
# Parse success or handle other errors...
try:
return json.loads(res['content'])
except json.JSONDecodeError as e:
self.error(f"Invalid JSON response: {e}")
self.errorState = True
return None
JSON Error Payload Parsing
Some APIs return 200 OK with embedded error codes. Modules must parse the response body:
data = json.loads(res['content'])
# Example: API-specific error code
if data.get("code") == "AUTH_FAILURE":
self.error(f"Authentication failed: {data.get('message')}")
self.errorState = True
return None
Complete Working Example
Here's a consolidated module snippet demonstrating all authentication handling patterns:
import json
from spiderfoot import SpiderFootEvent, SpiderFootPlugin
class sfp_example(SpiderFootPlugin):
meta = {
'name': "Example Module",
'options': {
'api_key': '',
'_fetchtimeout': 30,
'_useragent': 'SpiderFoot'
}
}
def setup(self, sfc, userOpts=dict()):
self.sf = sfc
self.errorState = False
self.__dataSource__ = "Example API"
self.results = self.tempStorage()
for opt in userOpts:
self.opts[opt] = userOpts[opt]
def watchedEvents(self):
return ["IP_ADDRESS"]
def producedEvents(self):
return ["MALICIOUS_IPADDR"]
def handleEvent(self, event):
# Error circuit breaker
if self.errorState:
return
eventName = event.eventType
srcModuleName = event.module
eventData = event.data
# Validate API key presence
if self.opts["api_key"] == "":
self.error(
f"You enabled {self.__class__.__name__} but did not set an API key!"
)
self.errorState = True
return
# Make API request
res = self.sf.fetchUrl(
f"https://api.example.com/v1/check?ip={eventData}",
headers={"X-API-Key": self.opts["api_key"]},
timeout=self.opts['_fetchtimeout'],
useragent=self.opts['_useragent']
)
if res['content'] is None:
return
# Handle HTTP-level authentication failures
if res['code'] == "401":
self.error("API key rejected — authentication failed.")
self.errorState = True
return
if res['code'] == "429":
self.error("Rate limit exceeded — throttling detected.")
self.errorState = True
return
# Parse and validate response
try:
data = json.loads(res['content'])
except json.JSONDecodeError as e:
self.error(f"Could not parse API response: {e}")
return
# Handle application-level errors
if data.get("status") == "error":
if data.get("code") == "INVALID_KEY":
self.error("API reports invalid key.")
self.errorState = True
else:
self.error(f"API error: {data.get('message')}")
return
# Process successful response...
if data.get("malicious"):
evt = SpiderFootEvent(
"MALICIOUS_IPADDR",
f"{eventData} [{data['threat_score']}]",
self.__name__,
event
)
self.notifyListeners(evt)
Key Source Files and Reference Locations
| File | Lines | Purpose |
|---|---|---|
[modules/sfp_virustotal.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_virustotal.py#L184-L186) |
184–186 | Missing API key validation |
[modules/sfp_virustotal.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_virustotal.py#L151-L154) |
151–154 | Rate limit (HTTP 204) handling |
[modules/sfp_xforce.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_xforce.py#L215-L216) |
215–216 | Multi-field credential check |
[modules/sfp_zonefiles.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_zonefiles.py#L154-L156) |
154–156 | Empty key guard pattern |
[spiderfoot/plugin.py](https://github.com/smicallef/spiderfoot/blob/master/spiderfoot/plugin.py) |
— | Base SpiderFootPlugin class with errorState definition |
Summary
- Check credentials first — Validate
api_keyand related fields inhandleEvent()before any network request. - Use
errorStateas a circuit breaker — Setself.errorState = Trueon any authentication or fatal error, then return immediately. - Inspect HTTP codes and response bodies — Handle 401/403 for auth failures, 429/204 for throttling, and parse JSON error payloads.
- Fail fast with clear messages — Call
self.error()with actionable descriptions so users understand configuration problems. - Prevent redundant requests — The
errorStatepattern ensures modules stop querying after the first failure, preserving API quotas and scan performance.
Frequently Asked Questions
How does SpiderFoot prevent modules from hammering APIs after an authentication failure?
Each module inherits errorState from SpiderFootPlugin. When set to True, the initial guard clause in handleEvent() returns immediately, skipping all further event processing for that module during the scan. This single flag prevents redundant network requests and protects API rate limits.
What HTTP status codes do SpiderFoot modules typically check for authentication errors?
Common checks include 401 (Unauthorized) and 403 (Forbidden) for invalid credentials, plus 429 (Too Many Requests) and 204 (No Content, used by VirusTotal) for throttling. Modules like [sfp_virustotal.py](https://github.com/smicallef/spiderfoot/blob/master/modules/sfp_virustotal.py#L151-L154) demonstrate these patterns explicitly.
Why do some modules check for API keys in handleEvent() rather than setup()?
SpiderFoot calls setup() once per module initialization, but users may change options during configuration. Checking in handleEvent() ensures the latest opts values are validated immediately before use. This runtime check catches configuration errors that would otherwise cause silent failures.
Can I customize error handling for a third-party API with unique error formats?
Yes. Override handleEvent() or create a dedicated query method that parses your API's specific error responses—whether JSON fields, HTTP headers, or status codes. Follow the established pattern: log via self.error(), set self.errorState = True, and return early to halt further processing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →