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 with context 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.py creates one httpx.AsyncClient reused by all modules
  • Async coroutine signature: Every module accepts (email, client, out) parameters
  • Browser emulation: Headers built from holehe/localuseragent.py rotate realistic User-Agents
  • Two-phase pattern: GET to extract tokens, then POST with email payload
  • Defensive error handling: All requests wrapped in try/except with rateLimit reporting
  • Uniform results: Standardized dictionary appended to shared out list

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →