uhttp-workers 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {uhttp_workers-1.0.0/uhttp_workers.egg-info → uhttp_workers-1.1.0}/PKG-INFO +48 -1
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/README.md +47 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/test_worker.py +98 -1
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/uhttp/workers.py +78 -10
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0/uhttp_workers.egg-info}/PKG-INFO +48 -1
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/.github/workflows/publish.yml +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/.github/workflows/tests.yml +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/.gitignore +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/examples/simple_workers.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/examples/static/index.html +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/pyproject.toml +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/setup.cfg +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/__init__.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/test_api_handler.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/test_decorators.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/test_dispatcher.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/test_pattern_matching.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/test_request_response.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/tests/test_worker_pool.py +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/uhttp_workers.egg-info/SOURCES.txt +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/uhttp_workers.egg-info/dependency_links.txt +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/uhttp_workers.egg-info/requires.txt +0 -0
- {uhttp_workers-1.0.0 → uhttp_workers-1.1.0}/uhttp_workers.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uhttp-workers
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Multi-process worker dispatcher built on uhttp-server
|
|
5
5
|
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -269,6 +269,7 @@ def process(self, request):
|
|
|
269
269
|
# request.query — {'page': '1'} or None
|
|
270
270
|
# request.data — dict (JSON), bytes (binary), or None
|
|
271
271
|
# request.headers — dict
|
|
272
|
+
# request.cookies — dict (lazy-parsed from Cookie header)
|
|
272
273
|
# request.content_type — 'application/json'
|
|
273
274
|
|
|
274
275
|
# return data (status 200)
|
|
@@ -315,6 +316,43 @@ class MyDispatcher(_workers.Dispatcher):
|
|
|
315
316
|
|
|
316
317
|
`do_check()` is only called for requests going to worker pools — static files and sync handlers are not affected.
|
|
317
318
|
|
|
319
|
+
### Worker-Level Validation
|
|
320
|
+
|
|
321
|
+
Override `do_check()` on the worker — runs before routing to handler:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
class MyWorker(_workers.Worker):
|
|
325
|
+
def do_check(self, request):
|
|
326
|
+
token = request.cookies.get('session')
|
|
327
|
+
if not token:
|
|
328
|
+
return {'error': 'unauthorized'}, 401
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Return `(data, status)` tuple to reject, or `None` to continue. You can also raise `RejectRequest`:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
def do_check(self, request):
|
|
335
|
+
if not self.validate_token(request.cookies.get('session')):
|
|
336
|
+
raise _workers.RejectRequest(
|
|
337
|
+
data={'error': 'forbidden'}, status=403)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`RejectRequest` accepts optional `data` (default: `{'error': 'Rejected'}`) and `status` (default: `403`).
|
|
341
|
+
|
|
342
|
+
### Error Handling
|
|
343
|
+
|
|
344
|
+
Override `on_request_error()` on the worker to customize error handling when a request handler raises an exception:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
class MyWorker(_workers.Worker):
|
|
348
|
+
def on_request_error(self, request, err):
|
|
349
|
+
if isinstance(err, DatabaseError):
|
|
350
|
+
self.db.reconnect()
|
|
351
|
+
return super().on_request_error(request, err)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Default behavior logs the error with traceback and returns 500.
|
|
355
|
+
|
|
318
356
|
## Post-Response Hook
|
|
319
357
|
|
|
320
358
|
Override `on_response()` on the dispatcher to post-process after a response is sent to the client — e.g., forward data to another worker pool:
|
|
@@ -394,6 +432,15 @@ Log messages are sent to the dispatcher via the shared response queue and printe
|
|
|
394
432
|
|
|
395
433
|
**Log levels:** `LOG_DEBUG` (10), `LOG_INFO` (20), `LOG_WARNING` (30), `LOG_ERROR` (40), `LOG_CRITICAL` (50)
|
|
396
434
|
|
|
435
|
+
Check current level with `is_*` properties to skip expensive formatting:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
if self.log.is_debug:
|
|
439
|
+
self.log.debug("Details: %s", expensive_computation())
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Available: `is_debug`, `is_info`, `is_warning`, `is_error`.
|
|
443
|
+
|
|
397
444
|
Set minimum level per pool:
|
|
398
445
|
|
|
399
446
|
```python
|
|
@@ -255,6 +255,7 @@ def process(self, request):
|
|
|
255
255
|
# request.query — {'page': '1'} or None
|
|
256
256
|
# request.data — dict (JSON), bytes (binary), or None
|
|
257
257
|
# request.headers — dict
|
|
258
|
+
# request.cookies — dict (lazy-parsed from Cookie header)
|
|
258
259
|
# request.content_type — 'application/json'
|
|
259
260
|
|
|
260
261
|
# return data (status 200)
|
|
@@ -301,6 +302,43 @@ class MyDispatcher(_workers.Dispatcher):
|
|
|
301
302
|
|
|
302
303
|
`do_check()` is only called for requests going to worker pools — static files and sync handlers are not affected.
|
|
303
304
|
|
|
305
|
+
### Worker-Level Validation
|
|
306
|
+
|
|
307
|
+
Override `do_check()` on the worker — runs before routing to handler:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
class MyWorker(_workers.Worker):
|
|
311
|
+
def do_check(self, request):
|
|
312
|
+
token = request.cookies.get('session')
|
|
313
|
+
if not token:
|
|
314
|
+
return {'error': 'unauthorized'}, 401
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Return `(data, status)` tuple to reject, or `None` to continue. You can also raise `RejectRequest`:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
def do_check(self, request):
|
|
321
|
+
if not self.validate_token(request.cookies.get('session')):
|
|
322
|
+
raise _workers.RejectRequest(
|
|
323
|
+
data={'error': 'forbidden'}, status=403)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`RejectRequest` accepts optional `data` (default: `{'error': 'Rejected'}`) and `status` (default: `403`).
|
|
327
|
+
|
|
328
|
+
### Error Handling
|
|
329
|
+
|
|
330
|
+
Override `on_request_error()` on the worker to customize error handling when a request handler raises an exception:
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
class MyWorker(_workers.Worker):
|
|
334
|
+
def on_request_error(self, request, err):
|
|
335
|
+
if isinstance(err, DatabaseError):
|
|
336
|
+
self.db.reconnect()
|
|
337
|
+
return super().on_request_error(request, err)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Default behavior logs the error with traceback and returns 500.
|
|
341
|
+
|
|
304
342
|
## Post-Response Hook
|
|
305
343
|
|
|
306
344
|
Override `on_response()` on the dispatcher to post-process after a response is sent to the client — e.g., forward data to another worker pool:
|
|
@@ -380,6 +418,15 @@ Log messages are sent to the dispatcher via the shared response queue and printe
|
|
|
380
418
|
|
|
381
419
|
**Log levels:** `LOG_DEBUG` (10), `LOG_INFO` (20), `LOG_WARNING` (30), `LOG_ERROR` (40), `LOG_CRITICAL` (50)
|
|
382
420
|
|
|
421
|
+
Check current level with `is_*` properties to skip expensive formatting:
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
if self.log.is_debug:
|
|
425
|
+
self.log.debug("Details: %s", expensive_computation())
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Available: `is_debug`, `is_info`, `is_warning`, `is_error`.
|
|
429
|
+
|
|
383
430
|
Set minimum level per pool:
|
|
384
431
|
|
|
385
432
|
```python
|
|
@@ -4,7 +4,9 @@ import time
|
|
|
4
4
|
import unittest
|
|
5
5
|
import multiprocessing as mp
|
|
6
6
|
|
|
7
|
-
from uhttp.workers import
|
|
7
|
+
from uhttp.workers import (
|
|
8
|
+
Worker, Request, Response, api, RejectRequest,
|
|
9
|
+
MSG_RESPONSE, MSG_HEARTBEAT)
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class SimpleWorker(Worker):
|
|
@@ -95,6 +97,101 @@ class TestWorkerHandleRequest(unittest.TestCase):
|
|
|
95
97
|
self.assertEqual(resp.status, 405)
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
class CheckWorker(Worker):
|
|
101
|
+
"""Worker with do_check that rejects unauthorized requests."""
|
|
102
|
+
|
|
103
|
+
def do_check(self, request):
|
|
104
|
+
token = request.cookies.get('session')
|
|
105
|
+
if not token:
|
|
106
|
+
return {'error': 'unauthorized'}, 401
|
|
107
|
+
|
|
108
|
+
@api('/secret', 'GET')
|
|
109
|
+
def secret(self, request):
|
|
110
|
+
return {'data': 'secret'}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RejectWorker(Worker):
|
|
114
|
+
"""Worker with do_check that raises RejectRequest."""
|
|
115
|
+
|
|
116
|
+
def do_check(self, request):
|
|
117
|
+
raise RejectRequest()
|
|
118
|
+
|
|
119
|
+
@api('/data', 'GET')
|
|
120
|
+
def data(self, request):
|
|
121
|
+
return {'data': 'ok'}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class FailCheckWorker(Worker):
|
|
125
|
+
"""Worker with do_check that raises unexpected exception."""
|
|
126
|
+
|
|
127
|
+
def do_check(self, request):
|
|
128
|
+
raise RuntimeError("check failed")
|
|
129
|
+
|
|
130
|
+
@api('/data', 'GET')
|
|
131
|
+
def data(self, request):
|
|
132
|
+
return {'data': 'ok'}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestWorkerDoCheck(unittest.TestCase):
|
|
136
|
+
|
|
137
|
+
def test_do_check_reject(self):
|
|
138
|
+
queues = [mp.Queue() for _ in range(3)]
|
|
139
|
+
worker = CheckWorker(0, *queues)
|
|
140
|
+
worker._build_routes()
|
|
141
|
+
req = Request(1, 'GET', '/secret')
|
|
142
|
+
resp = worker._handle_request(req)
|
|
143
|
+
self.assertEqual(resp.status, 401)
|
|
144
|
+
|
|
145
|
+
def test_do_check_pass(self):
|
|
146
|
+
queues = [mp.Queue() for _ in range(3)]
|
|
147
|
+
worker = CheckWorker(0, *queues)
|
|
148
|
+
worker._build_routes()
|
|
149
|
+
req = Request(1, 'GET', '/secret',
|
|
150
|
+
headers={'cookie': 'session=abc123'})
|
|
151
|
+
resp = worker._handle_request(req)
|
|
152
|
+
self.assertEqual(resp.status, 200)
|
|
153
|
+
self.assertEqual(resp.data, {'data': 'secret'})
|
|
154
|
+
|
|
155
|
+
def test_do_check_reject_request(self):
|
|
156
|
+
queues = [mp.Queue() for _ in range(3)]
|
|
157
|
+
worker = RejectWorker(0, *queues)
|
|
158
|
+
worker._build_routes()
|
|
159
|
+
req = Request(1, 'GET', '/data')
|
|
160
|
+
resp = worker._handle_request(req)
|
|
161
|
+
self.assertEqual(resp.status, 403)
|
|
162
|
+
|
|
163
|
+
def test_do_check_exception(self):
|
|
164
|
+
queues = [mp.Queue() for _ in range(3)]
|
|
165
|
+
worker = FailCheckWorker(0, *queues)
|
|
166
|
+
worker._build_routes()
|
|
167
|
+
req = Request(1, 'GET', '/data')
|
|
168
|
+
resp = worker._handle_request(req)
|
|
169
|
+
self.assertEqual(resp.status, 500)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestRequestCookies(unittest.TestCase):
|
|
173
|
+
|
|
174
|
+
def test_cookies_parsed(self):
|
|
175
|
+
req = Request(1, 'GET', '/',
|
|
176
|
+
headers={'cookie': 'session=abc; user=john'})
|
|
177
|
+
self.assertEqual(req.cookies, {'session': 'abc', 'user': 'john'})
|
|
178
|
+
|
|
179
|
+
def test_cookies_empty(self):
|
|
180
|
+
req = Request(1, 'GET', '/')
|
|
181
|
+
self.assertEqual(req.cookies, {})
|
|
182
|
+
|
|
183
|
+
def test_cookies_no_cookie_header(self):
|
|
184
|
+
req = Request(1, 'GET', '/', headers={'content-type': 'text/html'})
|
|
185
|
+
self.assertEqual(req.cookies, {})
|
|
186
|
+
|
|
187
|
+
def test_cookies_cached(self):
|
|
188
|
+
req = Request(1, 'GET', '/',
|
|
189
|
+
headers={'cookie': 'a=1'})
|
|
190
|
+
c1 = req.cookies
|
|
191
|
+
c2 = req.cookies
|
|
192
|
+
self.assertIs(c1, c2)
|
|
193
|
+
|
|
194
|
+
|
|
98
195
|
class TestWorkerMatchRoute(unittest.TestCase):
|
|
99
196
|
|
|
100
197
|
def setUp(self):
|
|
@@ -69,7 +69,11 @@ class ApiException(Exception):
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
class RejectRequest(ApiException):
|
|
72
|
-
"""Raised in do_check() to reject request
|
|
72
|
+
"""Raised in do_check() to reject request with custom status/data."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, data=None, status=403):
|
|
75
|
+
self.data = data if data is not None else {'error': 'Rejected'}
|
|
76
|
+
self.status = status
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
# Route decorator
|
|
@@ -199,7 +203,8 @@ class Request:
|
|
|
199
203
|
|
|
200
204
|
__slots__ = (
|
|
201
205
|
'request_id', 'method', 'path', 'query',
|
|
202
|
-
'data', 'headers', 'content_type', 'path_params'
|
|
206
|
+
'data', 'headers', 'content_type', 'path_params',
|
|
207
|
+
'_cookies')
|
|
203
208
|
|
|
204
209
|
def __init__(
|
|
205
210
|
self, request_id, method, path, query=None,
|
|
@@ -212,6 +217,16 @@ class Request:
|
|
|
212
217
|
self.headers = headers or {}
|
|
213
218
|
self.content_type = content_type
|
|
214
219
|
self.path_params = {}
|
|
220
|
+
self._cookies = None
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def cookies(self):
|
|
224
|
+
"""Cookies dict, parsed from Cookie header."""
|
|
225
|
+
if self._cookies is None:
|
|
226
|
+
raw = self.headers.get('cookie', '')
|
|
227
|
+
self._cookies = (
|
|
228
|
+
_uhttp_server.parse_cookies(raw) if raw else {})
|
|
229
|
+
return self._cookies
|
|
215
230
|
|
|
216
231
|
|
|
217
232
|
class Response:
|
|
@@ -271,6 +286,22 @@ class Logger:
|
|
|
271
286
|
self.level = level
|
|
272
287
|
self._queue = queue
|
|
273
288
|
|
|
289
|
+
@property
|
|
290
|
+
def is_debug(self):
|
|
291
|
+
return self.level <= LOG_DEBUG
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def is_info(self):
|
|
295
|
+
return self.level <= LOG_INFO
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def is_warning(self):
|
|
299
|
+
return self.level <= LOG_WARNING
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def is_error(self):
|
|
303
|
+
return self.level <= LOG_ERROR
|
|
304
|
+
|
|
274
305
|
def _log(self, level, msg, *args, **kwargs):
|
|
275
306
|
if level >= self.level:
|
|
276
307
|
try:
|
|
@@ -453,8 +484,37 @@ class Worker(_mp.Process):
|
|
|
453
484
|
if isinstance(msg, tuple) and msg[0] == CTL_CONFIG:
|
|
454
485
|
self.on_config(msg[1])
|
|
455
486
|
|
|
487
|
+
def do_check(self, request):
|
|
488
|
+
"""Validation hook called before routing request to handler.
|
|
489
|
+
|
|
490
|
+
Override for authentication, session validation, etc.
|
|
491
|
+
Raise RejectRequest to reject with custom response.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
request: Request object with headers, cookies, etc.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
None to continue, or (data, status) tuple to reject.
|
|
498
|
+
"""
|
|
499
|
+
|
|
456
500
|
def _handle_request(self, request):
|
|
457
501
|
"""Route and handle a single request, return Response."""
|
|
502
|
+
try:
|
|
503
|
+
result = self.do_check(request)
|
|
504
|
+
if result is not None:
|
|
505
|
+
data, status = result
|
|
506
|
+
return Response(request.request_id, data=data, status=status)
|
|
507
|
+
except RejectRequest as err:
|
|
508
|
+
return Response(
|
|
509
|
+
request.request_id,
|
|
510
|
+
data=err.data,
|
|
511
|
+
status=err.status)
|
|
512
|
+
except Exception as err:
|
|
513
|
+
self.log.error("do_check: %s\n%s", err, _traceback.format_exc())
|
|
514
|
+
return Response(
|
|
515
|
+
request.request_id,
|
|
516
|
+
data={'error': 'Internal server error'},
|
|
517
|
+
status=500)
|
|
458
518
|
handler = self._match_route(request)
|
|
459
519
|
if handler is None:
|
|
460
520
|
# check if path matches but method doesn't
|
|
@@ -477,14 +537,22 @@ class Worker(_mp.Process):
|
|
|
477
537
|
data, status = result, 200
|
|
478
538
|
return Response(request.request_id, data=data, status=status)
|
|
479
539
|
except Exception as err:
|
|
480
|
-
self.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
540
|
+
return self.on_request_error(request, err)
|
|
541
|
+
|
|
542
|
+
def on_request_error(self, request, err):
|
|
543
|
+
"""Called when request handler raises an exception.
|
|
544
|
+
|
|
545
|
+
Override to customize error handling (e.g., DB reconnect).
|
|
546
|
+
Default logs the error with traceback and returns 500 response.
|
|
547
|
+
"""
|
|
548
|
+
self.log.error(
|
|
549
|
+
"%s %s: %s\n%s",
|
|
550
|
+
request.method, request.path, err,
|
|
551
|
+
_traceback.format_exc())
|
|
552
|
+
return Response(
|
|
553
|
+
request.request_id,
|
|
554
|
+
data={'error': str(err)},
|
|
555
|
+
status=500)
|
|
488
556
|
|
|
489
557
|
def run(self):
|
|
490
558
|
"""Worker main loop using select for multiplexing."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uhttp-workers
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Multi-process worker dispatcher built on uhttp-server
|
|
5
5
|
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -269,6 +269,7 @@ def process(self, request):
|
|
|
269
269
|
# request.query — {'page': '1'} or None
|
|
270
270
|
# request.data — dict (JSON), bytes (binary), or None
|
|
271
271
|
# request.headers — dict
|
|
272
|
+
# request.cookies — dict (lazy-parsed from Cookie header)
|
|
272
273
|
# request.content_type — 'application/json'
|
|
273
274
|
|
|
274
275
|
# return data (status 200)
|
|
@@ -315,6 +316,43 @@ class MyDispatcher(_workers.Dispatcher):
|
|
|
315
316
|
|
|
316
317
|
`do_check()` is only called for requests going to worker pools — static files and sync handlers are not affected.
|
|
317
318
|
|
|
319
|
+
### Worker-Level Validation
|
|
320
|
+
|
|
321
|
+
Override `do_check()` on the worker — runs before routing to handler:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
class MyWorker(_workers.Worker):
|
|
325
|
+
def do_check(self, request):
|
|
326
|
+
token = request.cookies.get('session')
|
|
327
|
+
if not token:
|
|
328
|
+
return {'error': 'unauthorized'}, 401
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Return `(data, status)` tuple to reject, or `None` to continue. You can also raise `RejectRequest`:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
def do_check(self, request):
|
|
335
|
+
if not self.validate_token(request.cookies.get('session')):
|
|
336
|
+
raise _workers.RejectRequest(
|
|
337
|
+
data={'error': 'forbidden'}, status=403)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`RejectRequest` accepts optional `data` (default: `{'error': 'Rejected'}`) and `status` (default: `403`).
|
|
341
|
+
|
|
342
|
+
### Error Handling
|
|
343
|
+
|
|
344
|
+
Override `on_request_error()` on the worker to customize error handling when a request handler raises an exception:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
class MyWorker(_workers.Worker):
|
|
348
|
+
def on_request_error(self, request, err):
|
|
349
|
+
if isinstance(err, DatabaseError):
|
|
350
|
+
self.db.reconnect()
|
|
351
|
+
return super().on_request_error(request, err)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Default behavior logs the error with traceback and returns 500.
|
|
355
|
+
|
|
318
356
|
## Post-Response Hook
|
|
319
357
|
|
|
320
358
|
Override `on_response()` on the dispatcher to post-process after a response is sent to the client — e.g., forward data to another worker pool:
|
|
@@ -394,6 +432,15 @@ Log messages are sent to the dispatcher via the shared response queue and printe
|
|
|
394
432
|
|
|
395
433
|
**Log levels:** `LOG_DEBUG` (10), `LOG_INFO` (20), `LOG_WARNING` (30), `LOG_ERROR` (40), `LOG_CRITICAL` (50)
|
|
396
434
|
|
|
435
|
+
Check current level with `is_*` properties to skip expensive formatting:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
if self.log.is_debug:
|
|
439
|
+
self.log.debug("Details: %s", expensive_computation())
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Available: `is_debug`, `is_info`, `is_warning`, `is_error`.
|
|
443
|
+
|
|
397
444
|
Set minimum level per pool:
|
|
398
445
|
|
|
399
446
|
```python
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|