How a Holehe Module Makes HTTP Requests: Complete Async Architecture Guide
Holehe modules perform HTTP requests through a shared httpx.AsyncClient passed as an argument, enabling connection reuse and efficient asynchronous email verification across 100+ services.
The open-source OSINT tool Holehe by megadose checks whether an email address is registered on hundreds of websites. Understanding how Holehe modules make HTTP requests reveals a clean async design pattern built on Python's httpx library. This guide examines the actual source code to explain the request flow, error handling, and result collection mechanisms.
The Shared Async Client Architecture
Holehe avoids the overhead of creating new HTTP connections for every service check. Instead, it follows a single-client, multi-module pattern.
Central Client Initialization
In holehe/core.py, the main execution flow instantiates one httpx.AsyncClient that all modules share:
# holehe/core.py lines 12-14 (simplified)
client = httpx.AsyncClient(timeout=args.timeout)
# ...
for website in websites:
nursery.start_soon(launch_module, website, email, client, out)
This design provides three key benefits:
- Connection pooling – TCP connections are reused across requests to the same host
- Configurable timeout – Controlled via CLI argument (
--timeout) - Clean resource management – Single
async withcontext manager handles cleanup
Module Function Signature
Every Holehe module exports a coroutine with this exact signature:
# holehe/modules/social_media/facebook.py lines 6-9
async def facebook(email, client, out):
# email: str — the target email address
# client: httpx.AsyncClient — shared async client
# out: list — mutable result collection list
...
The client parameter is the same httpx.AsyncClient instance created in core.py. Modules never instantiate their own HTTP clients.
Constructing Requests with Browser Headers
Holehe modules masquerade as legitimate browsers to avoid trivial detection. The header construction process follows a consistent pattern.
User-Agent Rotation
Modules import random browser User-Agents from holehe/localuseragent.py:
# holehe/modules/social_media/facebook.py lines 10-11
from holehe.localuseragent import ua
random.choice(list(ua.data_browsers.keys()))
The ua object provides authentic browser strings for Chrome, Firefox, Safari, and Edge across multiple operating systems.
Header Assembly Example
Here's how the Facebook module builds its request headers:
# holehe/modules/social_media/facebook.py lines 12-19
headers = {
'User-Agent': random.choice(list(ua.data_browsers.keys())),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'DNT': '1',
'Connection': 'keep-alive',
}
Each module customizes headers based on the target service's expectations while maintaining realistic browser fingerprints.
The Complete Request Lifecycle: GET then POST
Holehe modules typically follow a two-phase request pattern: initial probe followed by submission with extracted tokens.
Phase 1: Initial GET Request
Modules first fetch a page to obtain session state:
# holehe/modules/social_media/facebook.py lines 22-27
try:
response = await client.get(
"https://www.facebook.com/login/identify",
headers=headers,
follow_redirects=True
)
This GET request serves multiple purposes:
- Establishes session cookies
- Retrieves CSRF tokens or other anti-authentication measures
- Confirms the endpoint is reachable
- Detects rate-limiting or blocking responses
Token Extraction
After the GET response, modules parse HTML or JavaScript to extract required tokens:
# Extract csrftoken from response (typical pattern)
token = re.search(r'"csrf_token":"([^"]+)"', response.text).group(1)
Phase 2: POST Request with Payload
With tokens acquired, modules submit the email address:
# holehe/modules/social_media/facebook.py lines 47-50
post_response = await client.post(
"https://www.facebook.com/login/identify",
data=payload,
headers=headers
)
The POST payload typically includes:
- The target email address
- Extracted CSRF tokens
- Additional form data required by the service
Response Processing and Result Reporting
Modules parse responses to determine registration status and handle edge cases.
JSON and Text Parsing
Depending on the service, modules use different parsing strategies:
# JSON API response
data = response.json()
# HTML/text response
text = response.text
Error Handling Pattern
All network operations are wrapped in comprehensive exception handling:
# holehe/modules/social_media/facebook.py lines 30-36 and 84-90
try:
# ... request code ...
except Exception:
out.append({
"name": "facebook",
"rateLimit": True,
"exists": False,
"emailrecovery": None,
"phoneNumber": None,
"others": None
})
return
On any exception—timeout, connection error, or parsing failure—the module reports rateLimit: True and returns immediately. This prevents one failing module from crashing the entire scan.
Result Dictionary Structure
Every module appends a standardized result dictionary to the shared out list:
| Key | Type | Description |
|---|---|---|
name |
str | Service identifier (e.g., "facebook") |
exists |
bool | Whether the email is registered |
rateLimit |
bool | Whether the check was blocked or failed |
emailrecovery |
str | None | Partial recovery email if exposed |
phoneNumber |
str | None | Partial phone number if exposed |
others |
dict | None | Additional service-specific data |
Complete Working Example
Running a Single Module Programmatically
import asyncio
import httpx
from holehe.modules.social_media.facebook import facebook
from holehe.localuseragent import ua
async def verify_email_on_facebook(email: str):
"""Manually run a single Holehe module with explicit client control."""
async with httpx.AsyncClient(timeout=10) as client:
results = []
await facebook(email, client, results)
return results
# Execute
results = asyncio.run(verify_email_on_facebook("target@example.com"))
print(results)
# Output: [{'name': 'facebook', 'exists': True, 'rateLimit': False, ...}]
Using the Official CLI
# Install Holehe
pip install holehe
# Run full scan with default settings
holehe target@example.com
# Customize timeout and output
holehe target@example.com --timeout 15 --only-used
Key Source Files Reference
| File | Purpose | Lines of Interest |
|---|---|---|
holehe/core.py |
Client creation and orchestration | 12-14 (client setup), nursery spawning |
holehe/localuseragent.py |
Browser User-Agent database | Full file—dictionary of realistic agents |
holehe/modules/social_media/facebook.py |
Exemplary module implementation | 6-11 (signature), 22-31 (GET), 47-52 (POST) |
Summary
- Single shared client:
holehe/core.pycreates onehttpx.AsyncClientreused by all modules - Async coroutine signature: Every module accepts
(email, client, out)parameters - Browser emulation: Headers built from
holehe/localuseragent.pyrotate realistic User-Agents - Two-phase pattern: GET to extract tokens, then POST with email payload
- Defensive error handling: All requests wrapped in
try/exceptwithrateLimitreporting - Uniform results: Standardized dictionary appended to shared
outlist
Frequently Asked Questions
What HTTP library does Holehe use instead of requests?
Holehe uses httpx rather than the synchronous requests library. The httpx.AsyncClient enables concurrent checking of hundreds of services without blocking, significantly improving performance over sequential requests.
Can I customize the timeout for slow connections?
Yes. The timeout is configurable via the --timeout CLI argument. Internally, this value is passed directly to httpx.AsyncClient(timeout=args.timeout) in holehe/core.py line 13. The default is typically 10 seconds.
How does Holehe avoid being blocked by services?
Modules employ multiple evasion techniques: rotating browser User-Agents from holehe/localuseragent.py, realistic header sets including Accept-Encoding and DNT flags, and mimicking legitimate browser request sequences (GET before POST). However, aggressive use may still trigger rate limits.
Why do modules use a shared out list instead of returning values?
The shared mutable list pattern allows holehe/core.py to collect results from hundreds of concurrently executing coroutines without complex synchronization primitives. Each module appends its result dictionary; the main loop awaits all tasks then processes the complete out list uniformly.
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 →