Last active
June 20, 2025 00:32
-
-
Save odysseus0/e787698e158cfe400f4dd82d272bad35 to your computer and use it in GitHub Desktop.
HTTP Client Detective - Systematically test Python HTTP clients to find which ones work with your API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # /// script | |
| # dependencies = [ | |
| # "httpx", | |
| # "aiohttp", | |
| # "requests", | |
| # ] | |
| # requires-python = ">=3.7" | |
| # /// | |
| """ | |
| HTTP Client Detective - Systematically test Python HTTP clients | |
| https://gist.github.com/odysseus0/e787698e158cfe400f4dd82d272bad35 | |
| This script helped me discover that httpx AsyncClient was the only | |
| Python HTTP client that failed with TikAPI, leading to a 3-hour | |
| debugging adventure that could have been avoided by just using aiohttp. | |
| Usage: uv run http_client_detective.py | |
| """ | |
| import asyncio | |
| import json | |
| import time | |
| import urllib.request | |
| from typing import Dict, Any, List | |
| import aiohttp | |
| import httpx | |
| import requests | |
| async def test_single_client(name: str, test_func, url: str, headers: Dict[str, str]) -> Dict[str, Any]: | |
| """Test a single HTTP client.""" | |
| print(f"\n{name}") | |
| try: | |
| start = time.time() | |
| await test_func(url, headers) | |
| duration = time.time() - start | |
| return { | |
| 'status': 'β SUCCESS', | |
| 'duration': f'{duration:.2f}s', | |
| 'response': 'success' | |
| } | |
| except Exception as e: | |
| result = { | |
| 'status': 'β FAILED', | |
| 'error': f"{type(e).__name__}: {e}" | |
| } | |
| # Special handling for httpx AsyncClient to show empty error | |
| if 'httpx' in name.lower() and 'async' in name.lower(): | |
| result['error_repr'] = repr(str(e)) | |
| return result | |
| async def test_urllib(url: str, headers: Dict[str, str]) -> None: | |
| """Test urllib (standard library).""" | |
| req = urllib.request.Request(url, headers=headers) | |
| response = urllib.request.urlopen(req, timeout=10) | |
| json.loads(response.read().decode()) | |
| async def test_requests(url: str, headers: Dict[str, str]) -> None: | |
| """Test requests library.""" | |
| response = requests.get(url, headers=headers, timeout=10) | |
| response.raise_for_status() | |
| response.json() | |
| async def test_httpx_sync(url: str, headers: Dict[str, str]) -> None: | |
| """Test httpx synchronous client.""" | |
| with httpx.Client(timeout=10) as client: | |
| response = client.get(url, headers=headers) | |
| response.raise_for_status() | |
| response.json() | |
| async def test_httpx_async(url: str, headers: Dict[str, str]) -> None: | |
| """Test httpx asynchronous client.""" | |
| async with httpx.AsyncClient(timeout=10) as client: | |
| response = await client.get(url, headers=headers) | |
| response.raise_for_status() | |
| response.json() | |
| async def test_aiohttp(url: str, headers: Dict[str, str]) -> None: | |
| """Test aiohttp library.""" | |
| timeout = aiohttp.ClientTimeout(total=10) | |
| async with aiohttp.ClientSession(timeout=timeout) as session: | |
| async with session.get(url, headers=headers) as response: | |
| response.raise_for_status() | |
| await response.json() | |
| async def test_all_clients(url: str, headers: Dict[str, str]) -> Dict[str, Any]: | |
| """Test all HTTP clients against the given URL.""" | |
| print(f"π Testing HTTP clients against: {url}") | |
| print("=" * 60) | |
| clients = [ | |
| ("Testing urllib (standard library)...", test_urllib), | |
| ("Testing requests...", test_requests), | |
| ("Testing httpx (sync)...", test_httpx_sync), | |
| ("Testing httpx (async)...", test_httpx_async), | |
| ("Testing aiohttp...", test_aiohttp), | |
| ] | |
| results = {} | |
| client_names = ['urllib', 'requests', 'httpx_sync', 'httpx_async', 'aiohttp'] | |
| for (name, test_func), client_name in zip(clients, client_names): | |
| results[client_name] = await test_single_client(name, test_func, url, headers) | |
| return results | |
| def print_results(results: Dict[str, Any]): | |
| """Pretty print the test results.""" | |
| print("\n" + "=" * 60) | |
| print("π TEST RESULTS") | |
| print("=" * 60) | |
| # Summary table | |
| print(f"\n{'Client':<15} {'Status':<12} {'Duration':<10} {'Details'}") | |
| print("-" * 60) | |
| for client, result in results.items(): | |
| status = result['status'] | |
| duration = result.get('duration', 'N/A') | |
| details = result.get('error', result.get('response', '')) | |
| if 'error_repr' in result: | |
| details += f" (repr: {result['error_repr']})" | |
| # Truncate long error messages | |
| if len(details) > 40: | |
| details = details[:37] + "..." | |
| print(f"{client:<15} {status:<12} {duration:<10} {details}") | |
| # Analysis | |
| print("\nπ ANALYSIS:") | |
| successes = [k for k, v in results.items() if v['status'] == 'β SUCCESS'] | |
| failures = [k for k, v in results.items() if v['status'] == 'β FAILED'] | |
| if successes: | |
| print(f"β Working clients: {', '.join(successes)}") | |
| if failures: | |
| print(f"β Failed clients: {', '.join(failures)}") | |
| # The key insight | |
| if 'httpx_async' in failures and 'httpx_sync' in successes: | |
| print("\nπ€ INTERESTING: httpx sync works but async fails!") | |
| print(" This suggests an async-specific issue (spoiler: IPv6)") | |
| if 'aiohttp' in successes and 'httpx_async' in failures: | |
| print("\nπ‘ SOLUTION: Just use aiohttp instead of httpx.AsyncClient!") | |
| async def main(): | |
| """Run the HTTP client detective. | |
| This tests against api.tikapi.io without an API key to demonstrate that: | |
| - Most HTTP clients get a 400 error (they successfully connect) | |
| - httpx AsyncClient fails with empty ConnectError (IPv6 TLS handshake fails) | |
| - aiohttp just works! | |
| """ | |
| # Test configuration | |
| test_cases = [ | |
| { | |
| 'name': 'TikAPI /public/check (without API key)', | |
| 'url': 'https://api.tikapi.io/public/check', | |
| 'headers': { | |
| 'Accept': 'application/json' | |
| } | |
| }, | |
| # Add more test cases here if needed | |
| ] | |
| for test in test_cases: | |
| print(f"\nπ― Test Case: {test['name']}") | |
| results = await test_all_clients(test['url'], test['headers']) | |
| print_results(results) | |
| print("\n" + "=" * 60) | |
| print("π Testing complete!") | |
| print("\nπ KEY INSIGHT:") | |
| print("httpx AsyncClient fails with empty ConnectError (IPv6 TLS handshake)") | |
| print("while all other clients work. The fix? Just use aiohttp! π") | |
| if __name__ == "__main__": | |
| asyncio.run(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment