0
# Event Hooks
1
2
Event hook system that allows custom functions to be called at specific points during request processing. Hooks provide a way to modify requests and responses or implement custom logging and monitoring.
3
4
## Capabilities
5
6
### Hook System
7
8
The requests library provides a simple but powerful hook system for intercepting and modifying HTTP requests and responses.
9
10
```python { .api }
11
# Available hook events
12
HOOKS: list[str] # ['response']
13
14
def default_hooks() -> dict:
15
"""
16
Return the default hooks structure.
17
18
Returns:
19
Dict with default hook configuration
20
"""
21
22
def dispatch_hook(key: str, hooks: dict, hook_data, **kwargs):
23
"""
24
Execute hooks for a given event.
25
26
Parameters:
27
- key: Hook event name
28
- hooks: Hook configuration dict
29
- hook_data: Data to pass to hook functions
30
- **kwargs: Additional arguments for hooks
31
32
Returns:
33
Result from hook execution
34
"""
35
```
36
37
### Hook Events
38
39
Currently, requests supports one hook event:
40
41
- **response**: Called after receiving a response, allows modifying the Response object
42
43
## Usage Examples
44
45
### Basic Hook Usage
46
47
```python
48
import requests
49
50
def response_hook(response, *args, **kwargs):
51
"""Custom response hook function."""
52
print(f"Received response: {response.status_code} from {response.url}")
53
# Optionally modify the response
54
response.custom_processed = True
55
return response
56
57
# Add hook to single request
58
response = requests.get('https://httpbin.org/get',
59
hooks={'response': response_hook})
60
61
print(hasattr(response, 'custom_processed')) # True
62
```
63
64
### Multiple Hooks
65
66
```python
67
import requests
68
69
def log_hook(response, *args, **kwargs):
70
"""Log response details."""
71
print(f"LOG: {response.request.method} {response.url} -> {response.status_code}")
72
return response
73
74
def timing_hook(response, *args, **kwargs):
75
"""Add timing information."""
76
print(f"TIMING: Request took {response.elapsed.total_seconds():.3f} seconds")
77
return response
78
79
def validation_hook(response, *args, **kwargs):
80
"""Validate response."""
81
if response.status_code >= 400:
82
print(f"WARNING: Error response {response.status_code}")
83
return response
84
85
# Multiple hooks for the same event
86
hooks = {
87
'response': [log_hook, timing_hook, validation_hook]
88
}
89
90
response = requests.get('https://httpbin.org/status/404', hooks=hooks)
91
# Prints:
92
# LOG: GET https://httpbin.org/status/404 -> 404
93
# TIMING: Request took 0.123 seconds
94
# WARNING: Error response 404
95
```
96
97
### Session-Level Hooks
98
99
```python
100
import requests
101
102
def session_response_hook(response, *args, **kwargs):
103
"""Hook applied to all session requests."""
104
# Add custom header to all responses
105
response.headers['X-Custom-Processed'] = 'true'
106
107
# Log all requests
108
print(f"Session request: {response.request.method} {response.url}")
109
110
return response
111
112
# Add hook to session - applies to all requests
113
session = requests.Session()
114
session.hooks['response'].append(session_response_hook)
115
116
# All requests through this session will trigger the hook
117
response1 = session.get('https://httpbin.org/get')
118
response2 = session.post('https://httpbin.org/post', json={'key': 'value'})
119
120
print(response1.headers['X-Custom-Processed']) # 'true'
121
print(response2.headers['X-Custom-Processed']) # 'true'
122
```
123
124
### Authentication Hook
125
126
```python
127
import requests
128
import hashlib
129
import time
130
131
def custom_auth_hook(response, *args, **kwargs):
132
"""Custom authentication logic."""
133
if response.status_code == 401:
134
print("Authentication required - could trigger token refresh")
135
# In real implementation, might refresh token and retry request
136
return response
137
138
def rate_limit_hook(response, *args, **kwargs):
139
"""Handle rate limiting."""
140
if response.status_code == 429:
141
retry_after = response.headers.get('Retry-After', '60')
142
print(f"Rate limited. Retry after {retry_after} seconds")
143
# Could implement automatic retry with backoff
144
return response
145
146
# Combine authentication and rate limiting hooks
147
auth_hooks = {
148
'response': [custom_auth_hook, rate_limit_hook]
149
}
150
151
session = requests.Session()
152
session.hooks = auth_hooks
153
154
response = session.get('https://httpbin.org/status/429')
155
# Prints: Rate limited. Retry after 60 seconds
156
```
157
158
### Request Modification Hook
159
160
```python
161
import requests
162
163
def add_user_agent_hook(response, *args, **kwargs):
164
"""Add custom user agent information to response for logging."""
165
original_ua = response.request.headers.get('User-Agent', 'Unknown')
166
print(f"Request made with User-Agent: {original_ua}")
167
168
# Add metadata to response for later use
169
response.user_agent_info = {
170
'original_ua': original_ua,
171
'timestamp': time.time()
172
}
173
return response
174
175
# Apply hook
176
response = requests.get('https://httpbin.org/user-agent',
177
hooks={'response': add_user_agent_hook})
178
179
print(response.user_agent_info)
180
```
181
182
### Error Handling Hook
183
184
```python
185
import requests
186
import logging
187
188
# Set up logging
189
logging.basicConfig(level=logging.INFO)
190
logger = logging.getLogger(__name__)
191
192
def error_logging_hook(response, *args, **kwargs):
193
"""Log errors and add context."""
194
if response.status_code >= 400:
195
logger.error(f"HTTP Error {response.status_code}: {response.url}")
196
logger.error(f"Response: {response.text[:200]}...")
197
198
# Add error context to response
199
response.error_logged = True
200
response.error_timestamp = time.time()
201
202
return response
203
204
def retry_header_hook(response, *args, **kwargs):
205
"""Extract retry information from headers."""
206
if response.status_code in [429, 503]:
207
retry_after = response.headers.get('Retry-After')
208
if retry_after:
209
response.retry_after_seconds = int(retry_after)
210
logger.info(f"Server suggests retry after {retry_after} seconds")
211
212
return response
213
214
# Error handling hook chain
215
error_hooks = {
216
'response': [error_logging_hook, retry_header_hook]
217
}
218
219
response = requests.get('https://httpbin.org/status/503', hooks=error_hooks)
220
print(f"Error logged: {getattr(response, 'error_logged', False)}")
221
```
222
223
### Performance Monitoring Hook
224
225
```python
226
import requests
227
import time
228
229
class PerformanceMonitor:
230
"""Performance monitoring via hooks."""
231
232
def __init__(self):
233
self.stats = {}
234
235
def response_hook(self, response, *args, **kwargs):
236
"""Monitor response performance."""
237
url = response.url
238
elapsed = response.elapsed.total_seconds()
239
status = response.status_code
240
size = len(response.content)
241
242
# Track stats
243
if url not in self.stats:
244
self.stats[url] = []
245
246
self.stats[url].append({
247
'elapsed': elapsed,
248
'status': status,
249
'size': size,
250
'timestamp': time.time()
251
})
252
253
print(f"PERF: {url} -> {status} ({elapsed:.3f}s, {size} bytes)")
254
return response
255
256
def get_stats(self):
257
"""Get performance statistics."""
258
return self.stats
259
260
# Use performance monitor
261
monitor = PerformanceMonitor()
262
263
session = requests.Session()
264
session.hooks['response'].append(monitor.response_hook)
265
266
# Make several requests
267
urls = [
268
'https://httpbin.org/get',
269
'https://httpbin.org/json',
270
'https://httpbin.org/html'
271
]
272
273
for url in urls:
274
response = session.get(url)
275
276
# View stats
277
stats = monitor.get_stats()
278
for url, measurements in stats.items():
279
avg_time = sum(m['elapsed'] for m in measurements) / len(measurements)
280
print(f"Average time for {url}: {avg_time:.3f}s")
281
```
282
283
### Custom Response Processing Hook
284
285
```python
286
import requests
287
import json
288
289
def json_response_hook(response, *args, **kwargs):
290
"""Automatically parse JSON responses."""
291
content_type = response.headers.get('content-type', '')
292
293
if 'application/json' in content_type:
294
try:
295
response.parsed_json = response.json()
296
response.json_parsed = True
297
except ValueError:
298
response.json_parsed = False
299
response.parsed_json = None
300
else:
301
response.json_parsed = False
302
response.parsed_json = None
303
304
return response
305
306
def xml_response_hook(response, *args, **kwargs):
307
"""Handle XML responses."""
308
content_type = response.headers.get('content-type', '')
309
310
if 'xml' in content_type:
311
response.is_xml = True
312
# Could parse XML here with lxml or xml module
313
else:
314
response.is_xml = False
315
316
return response
317
318
# Content processing hooks
319
content_hooks = {
320
'response': [json_response_hook, xml_response_hook]
321
}
322
323
response = requests.get('https://httpbin.org/json', hooks=content_hooks)
324
print(f"JSON parsed: {response.json_parsed}")
325
print(f"Data: {response.parsed_json}")
326
```
327
328
### Hook Registration Patterns
329
330
```python
331
import requests
332
333
# Pattern 1: Direct function assignment
334
def my_hook(response, *args, **kwargs):
335
return response
336
337
hooks = {'response': my_hook}
338
339
# Pattern 2: List of functions
340
hooks = {'response': [hook1, hook2, hook3]}
341
342
# Pattern 3: Adding to existing hooks
343
session = requests.Session()
344
session.hooks['response'].append(my_hook)
345
346
# Pattern 4: Replacing all hooks
347
session.hooks['response'] = [my_hook]
348
349
# Pattern 5: Using Request object
350
req = requests.Request('GET', 'https://httpbin.org/get', hooks=hooks)
351
prepared = req.prepare()
352
353
with requests.Session() as session:
354
response = session.send(prepared)
355
```
356
357
## Hook Best Practices
358
359
### Hook Function Signature
360
361
```python
362
def my_hook(response, *args, **kwargs):
363
"""
364
Standard hook function signature.
365
366
Parameters:
367
- response: Response object being processed
368
- *args: Additional positional arguments
369
- **kwargs: Additional keyword arguments
370
371
Returns:
372
Modified or original Response object
373
"""
374
# Process response
375
return response
376
```
377
378
### Error Handling in Hooks
379
380
```python
381
def safe_hook(response, *args, **kwargs):
382
"""Hook with proper error handling."""
383
try:
384
# Hook logic here
385
response.custom_data = process_response(response)
386
except Exception as e:
387
# Log error but don't break the request
388
print(f"Hook error: {e}")
389
response.hook_error = str(e)
390
391
return response
392
```
393
394
### Performance Considerations
395
396
```python
397
def efficient_hook(response, *args, **kwargs):
398
"""Efficient hook implementation."""
399
# Only process if needed
400
if should_process(response):
401
# Minimal processing
402
response.processed = True
403
404
return response
405
406
def should_process(response):
407
"""Determine if processing is needed."""
408
return response.headers.get('content-type') == 'application/json'
409
```
410
411
## Hook Limitations
412
413
1. **Limited Events**: Only 'response' hook is currently supported
414
2. **No Request Hooks**: Cannot modify requests before sending
415
3. **Exception Handling**: Hook exceptions can break request processing
416
4. **Performance Impact**: Hooks add processing overhead
417
5. **State Management**: Hooks are stateless - use closures or classes for state
418
419
## Advanced Hook Patterns
420
421
### Hook Factory
422
423
```python
424
def create_logging_hook(logger_name):
425
"""Factory function to create logging hooks."""
426
import logging
427
logger = logging.getLogger(logger_name)
428
429
def logging_hook(response, *args, **kwargs):
430
logger.info(f"{response.request.method} {response.url} -> {response.status_code}")
431
return response
432
433
return logging_hook
434
435
# Create specialized hooks
436
api_hook = create_logging_hook('api_client')
437
web_hook = create_logging_hook('web_scraper')
438
439
# Use different hooks for different purposes
440
api_session = requests.Session()
441
api_session.hooks['response'].append(api_hook)
442
443
web_session = requests.Session()
444
web_session.hooks['response'].append(web_hook)
445
```
446
447
### Conditional Hooks
448
449
```python
450
def conditional_hook(condition_func):
451
"""Create a hook that only runs when condition is met."""
452
def decorator(hook_func):
453
def wrapper(response, *args, **kwargs):
454
if condition_func(response):
455
return hook_func(response, *args, **kwargs)
456
return response
457
return wrapper
458
return decorator
459
460
# Conditional hook decorators
461
@conditional_hook(lambda r: r.status_code >= 400)
462
def error_only_hook(response, *args, **kwargs):
463
print(f"Error response: {response.status_code}")
464
return response
465
466
@conditional_hook(lambda r: 'json' in r.headers.get('content-type', ''))
467
def json_only_hook(response, *args, **kwargs):
468
print("Processing JSON response")
469
return response
470
```