Skip to content

Commit 056a8d6

Browse files
authored
chore: updated version and benchmarking script (#25)
1 parent f3a6044 commit 056a8d6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4577
-12625
lines changed

README.md

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -231,25 +231,6 @@ except httpmorph.RequestException as e:
231231
print(f"Request failed: {e}")
232232
```
233233

234-
## Performance
235-
236-
httpmorph is built for speed with a native C implementation and BoringSSL for optimized TLS operations.
237-
238-
**Benchmark vs requests library** (30-second test, macOS Apple Silicon):
239-
- **Throughput:** 49.4% faster on average (1,056 vs 707 req/s)
240-
- **GET requests:** 1,032 req/s vs 709 req/s (1.46x faster)
241-
- **POST requests:** 1,080 req/s vs 705 req/s (1.53x faster)
242-
- **Latency:** 33.1% lower on average (0.95ms vs 1.41ms)
243-
244-
See [detailed benchmark results](benchmarks/results_requests_macos.md) for full metrics including performance charts.
245-
246-
### Performance Features
247-
- Native C implementation with minimal Python overhead
248-
- BoringSSL for optimized TLS operations
249-
- Connection pooling reduces handshake overhead
250-
- HTTP/2 multiplexing for concurrent requests
251-
- Efficient memory management
252-
253234
## Platform Support
254235

255236
| Platform | Status |
@@ -383,8 +364,8 @@ pytest tests/ -v
383364

384365
- Built on BoringSSL (Google)
385366
- HTTP/2 support via nghttp2
386-
- Inspired by Python's requests library
387-
- Browser fingerprints based on real browser implementations
367+
- Inspired by Python's requests and httpx libraries
368+
- Browser fingerprints based on real browser implementations ( still in progress )
388369

389370
## FAQ
390371

@@ -403,7 +384,8 @@ A: Please open an issue on GitHub with a minimal reproduction example and your e
403384
## Support
404385

405386
- GitHub Issues: [Report bugs and feature requests](https://github.com/arman-bd/httpmorph/issues)
406-
- Documentation: [Full API documentation](https://httpmorph.readthedocs.io) (coming soon)
387+
- Documentation: [Full API documentation](https://httpmorph.readthedocs.io)
388+
- PyPI: [httpmorph on PyPI](https://pypi.org/project/httpmorph/)
407389

408390
---
409391

benchmarks/benchmark.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,8 +1803,8 @@ def main():
18031803
"-s",
18041804
"--sequential",
18051805
type=int,
1806-
default=50,
1807-
help="Number of sequential requests (default: 50)",
1806+
default=25,
1807+
help="Number of sequential requests (default: 25)",
18081808
)
18091809
parser.add_argument(
18101810
"-c",
@@ -1814,7 +1814,7 @@ def main():
18141814
help="Number of concurrent requests (default: 25)",
18151815
)
18161816
parser.add_argument(
1817-
"-w", "--workers", type=int, default=10, help="Number of concurrent workers (default: 10)"
1817+
"-w", "--workers", type=int, default=5, help="Number of concurrent workers (default: 5)"
18181818
)
18191819
parser.add_argument(
18201820
"--warmup", type=int, default=5, help="Number of warmup requests (default: 5)"

benchmarks/libs/aiohttp_bench.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ async def request():
3232
self.local_url, timeout=aiohttp.ClientTimeout(total=10)
3333
) as resp:
3434
assert 200 <= resp.status < 600, f"Got status {resp.status}"
35-
await resp.read()
35+
body = await resp.text()
36+
self.validate_response_body(self.local_url, body)
3637

3738
return self.run_async_benchmark(request)
3839

@@ -43,7 +44,8 @@ async def request():
4344
self.remote_http_url, timeout=aiohttp.ClientTimeout(total=10)
4445
) as resp:
4546
assert 200 <= resp.status < 600, f"Got status {resp.status}"
46-
await resp.read()
47+
body = await resp.text()
48+
self.validate_response_body(self.remote_http_url, body)
4749

4850
return self.run_async_benchmark(request)
4951

@@ -54,7 +56,8 @@ async def request():
5456
self.remote_https_url, timeout=aiohttp.ClientTimeout(total=10)
5557
) as resp:
5658
assert 200 <= resp.status < 600, f"Got status {resp.status}"
57-
await resp.read()
59+
body = await resp.text()
60+
self.validate_response_body(self.remote_https_url, body)
5861

5962
return self.run_async_benchmark(request)
6063

@@ -73,7 +76,8 @@ async def request():
7376
timeout=aiohttp.ClientTimeout(total=10),
7477
) as resp:
7578
assert 200 <= resp.status < 600, f"Got status {resp.status}"
76-
await resp.read()
79+
body = await resp.text()
80+
self.validate_response_body(self.proxy_target_http, body)
7781
except Exception:
7882
pass
7983

@@ -97,7 +101,8 @@ async def request():
97101
timeout=aiohttp.ClientTimeout(total=10),
98102
) as resp:
99103
assert 200 <= resp.status < 600, f"Got status {resp.status}"
100-
await resp.read()
104+
body = await resp.text()
105+
self.validate_response_body(self.proxy_target_https, body)
101106
except Exception:
102107
pass
103108

benchmarks/libs/base.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
from abc import ABC, abstractmethod
77
from concurrent.futures import ThreadPoolExecutor
8+
from urllib.parse import urlparse
89

910

1011
class LibraryBenchmark(ABC):
@@ -41,6 +42,34 @@ def __init__(self, config):
4142
self.proxy_target_http = config["proxy_target_http"]
4243
self.proxy_target_https = config["proxy_target_https"]
4344

45+
def validate_response_body(self, url, body_text):
46+
"""
47+
Validate that the response body contains the hostname from the URL.
48+
49+
Args:
50+
url: The requested URL (e.g., "http://httpbin.org/get")
51+
body_text: The response body as text
52+
53+
Raises:
54+
AssertionError: If the hostname is not found in the response body
55+
"""
56+
try:
57+
parsed = urlparse(url)
58+
hostname = parsed.hostname or parsed.netloc.split('@')[-1].split(':')[0]
59+
60+
if not hostname or hostname in ['127.0.0.1', 'localhost']:
61+
# Skip validation for local URLs
62+
return
63+
64+
# Check if hostname appears in the response body
65+
assert hostname in body_text, (
66+
f"Validation failed: hostname '{hostname}' not found in response body. "
67+
f"This might indicate proxy misconfiguration or incorrect routing."
68+
)
69+
except (AttributeError, IndexError) as e:
70+
# If URL parsing fails, skip validation
71+
pass
72+
4473
@abstractmethod
4574
def get_test_matrix(self):
4675
"""Return list of (test_name, test_key) tuples for this library"""

benchmarks/libs/curl_cffi_bench.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def run():
4040

4141
resp = curl_requests.get(self.local_url, verify=False)
4242
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
43+
self.validate_response_body(self.local_url, resp.text)
4344

4445
return self.run_sequential_benchmark("curl_cffi_seq_local", run)
4546

@@ -49,6 +50,7 @@ def run():
4950

5051
resp = curl_requests.get(self.remote_http_url)
5152
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
53+
self.validate_response_body(self.remote_http_url, resp.text)
5254

5355
return self.run_sequential_benchmark("curl_cffi_seq_remote_http", run)
5456

@@ -58,6 +60,7 @@ def run():
5860

5961
resp = curl_requests.get(self.remote_https_url, verify=False)
6062
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
63+
self.validate_response_body(self.remote_https_url, resp.text)
6164

6265
return self.run_sequential_benchmark("curl_cffi_seq_remote_https", run)
6366

@@ -68,6 +71,7 @@ def run():
6871
# curl_cffi uses HTTP/2 by default when available
6972
resp = curl_requests.get(self.http2_url, verify=False, timeout=10)
7073
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
74+
self.validate_response_body(self.http2_url, resp.text)
7175

7276
return self.run_sequential_benchmark("curl_cffi_seq_remote_http2", run)
7377

@@ -77,6 +81,7 @@ def run():
7781

7882
resp = curl_requests.get(self.local_url, verify=False)
7983
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
84+
self.validate_response_body(self.local_url, resp.text)
8085

8186
return self.run_concurrent_sync(run)
8287

@@ -86,6 +91,7 @@ def run():
8691

8792
resp = curl_requests.get(self.remote_http_url)
8893
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
94+
self.validate_response_body(self.remote_http_url, resp.text)
8995

9096
return self.run_concurrent_sync(run)
9197

@@ -95,6 +101,7 @@ def run():
95101

96102
resp = curl_requests.get(self.remote_https_url, verify=False)
97103
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
104+
self.validate_response_body(self.remote_https_url, resp.text)
98105

99106
return self.run_concurrent_sync(run)
100107

@@ -104,6 +111,7 @@ def run():
104111

105112
resp = curl_requests.get(self.http2_url, verify=False, timeout=10)
106113
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
114+
self.validate_response_body(self.http2_url, resp.text)
107115

108116
return self.run_concurrent_sync(run)
109117

benchmarks/libs/httpmorph_bench.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def seq_local_http(self):
5050
def run():
5151
resp = client.get(self.local_url, verify=False)
5252
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
53+
self.validate_response_body(self.local_url, resp.text)
5354

5455
return self.run_sequential_benchmark("httpmorph_seq_local_http", run)
5556

@@ -59,6 +60,7 @@ def seq_remote_http(self):
5960
def run():
6061
resp = client.get(self.remote_http_url, verify=False, timeout=10)
6162
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
63+
self.validate_response_body(self.remote_http_url, resp.text)
6264

6365
return self.run_sequential_benchmark("httpmorph_seq_remote_http", run)
6466

@@ -68,6 +70,7 @@ def seq_remote_https(self):
6870
def run():
6971
resp = client.get(self.remote_https_url, verify=False, timeout=10)
7072
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
73+
self.validate_response_body(self.remote_https_url, resp.text)
7174

7275
return self.run_sequential_benchmark("httpmorph_seq_remote_https", run)
7376

@@ -81,6 +84,7 @@ def seq_remote_http2(self):
8184
def run():
8285
resp = client.get(self.http2_url, verify=False, timeout=10)
8386
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
87+
self.validate_response_body(self.http2_url, resp.text)
8488

8589
return self.run_sequential_benchmark("httpmorph_seq_remote_http2", run)
8690

@@ -95,6 +99,7 @@ def run():
9599
self.proxy_target_http, proxy=self.proxy_url_http, verify=False, timeout=10
96100
)
97101
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
102+
self.validate_response_body(self.proxy_target_http, resp.text)
98103
except Exception:
99104
# Proxy can be slow, accept timeout as valid
100105
pass
@@ -121,6 +126,7 @@ def run():
121126
self.proxy_target_https, proxy=self.proxy_url_https, verify=False, timeout=10
122127
)
123128
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
129+
self.validate_response_body(self.proxy_target_https, resp.text)
124130
except Exception:
125131
# Proxy can be slow, accept timeout as valid
126132
pass
@@ -149,6 +155,7 @@ def run():
149155
self.proxy_target_https, proxy=self.proxy_url_https, verify=False, timeout=10
150156
)
151157
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
158+
self.validate_response_body(self.proxy_target_https, resp.text)
152159
except Exception:
153160
pass
154161

@@ -170,6 +177,7 @@ def conc_local_http(self):
170177
def run():
171178
resp = client.get(self.local_url, verify=False)
172179
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
180+
self.validate_response_body(self.local_url, resp.text)
173181

174182
return self.run_concurrent_sync(run)
175183

@@ -179,6 +187,7 @@ def conc_remote_http(self):
179187
def run():
180188
resp = client.get(self.remote_http_url, verify=False, timeout=10)
181189
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
190+
self.validate_response_body(self.remote_http_url, resp.text)
182191

183192
return self.run_concurrent_sync(run)
184193

@@ -188,6 +197,7 @@ def conc_remote_https(self):
188197
def run():
189198
resp = client.get(self.remote_https_url, verify=False, timeout=10)
190199
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
200+
self.validate_response_body(self.remote_https_url, resp.text)
191201

192202
return self.run_concurrent_sync(run)
193203

@@ -200,6 +210,7 @@ def conc_remote_http2(self):
200210
def run():
201211
resp = client.get(self.http2_url, verify=False, timeout=10)
202212
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
213+
self.validate_response_body(self.http2_url, resp.text)
203214

204215
return self.run_concurrent_sync(run)
205216

@@ -214,6 +225,7 @@ def run():
214225
self.proxy_target_http, proxy=self.proxy_url_http, verify=False, timeout=10
215226
)
216227
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
228+
self.validate_response_body(self.proxy_target_http, resp.text)
217229
except Exception:
218230
# Proxy can be slow or rate-limit concurrent connections, accept timeout as valid
219231
pass
@@ -235,6 +247,7 @@ def run():
235247
self.proxy_target_https, proxy=self.proxy_url_https, verify=False, timeout=10
236248
)
237249
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
250+
self.validate_response_body(self.proxy_target_https, resp.text)
238251
except Exception:
239252
# Proxy can be slow or rate-limit concurrent connections, accept timeout as valid
240253
pass
@@ -259,6 +272,7 @@ def run():
259272
self.proxy_target_https, proxy=self.proxy_url_https, verify=False, timeout=10
260273
)
261274
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
275+
self.validate_response_body(self.proxy_target_https, resp.text)
262276
except Exception:
263277
# HTTP/2 over proxy can be slow, accept timeout as valid
264278
pass
@@ -280,6 +294,7 @@ async def run_batch():
280294
async def request():
281295
resp = await client.get(self.local_url, timeout=10)
282296
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
297+
self.validate_response_body(self.local_url, resp.text)
283298

284299
return await self.run_concurrent_async(request)
285300

@@ -298,6 +313,7 @@ async def run_batch():
298313
async def request():
299314
resp = await client.get(self.remote_http_url, verify=False, timeout=10)
300315
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
316+
self.validate_response_body(self.remote_http_url, resp.text)
301317

302318
return await self.run_concurrent_async(request)
303319

@@ -316,6 +332,7 @@ async def run_batch():
316332
async def request():
317333
resp = await client.get(self.remote_https_url, verify=False, timeout=10)
318334
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
335+
self.validate_response_body(self.remote_https_url, resp.text)
319336

320337
return await self.run_concurrent_async(request)
321338

@@ -336,6 +353,7 @@ async def run_batch():
336353
async def request():
337354
resp = await client.get(self.http2_url, verify=False, timeout=10)
338355
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
356+
self.validate_response_body(self.http2_url, resp.text)
339357

340358
return await self.run_concurrent_async(request)
341359

@@ -362,6 +380,7 @@ async def request():
362380
timeout=10,
363381
)
364382
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
383+
self.validate_response_body(self.proxy_target_http, resp.text)
365384
except Exception:
366385
# Proxy can be slow, accept timeout as valid
367386
pass
@@ -392,6 +411,7 @@ async def request():
392411
timeout=10,
393412
)
394413
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
414+
self.validate_response_body(self.proxy_target_https, resp.text)
395415
except Exception:
396416
# Proxy can be slow, accept timeout as valid
397417
pass
@@ -424,6 +444,7 @@ async def request():
424444
timeout=10,
425445
)
426446
assert 200 <= resp.status_code < 600, f"Got status {resp.status_code}"
447+
self.validate_response_body(self.proxy_target_https, resp.text)
427448
except Exception:
428449
# HTTP/2 over proxy can be slow, accept timeout as valid
429450
pass

0 commit comments

Comments
 (0)