httpdbg 1.1.2__tar.gz → 1.2.1__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.
- {httpdbg-1.1.2 → httpdbg-1.2.1}/PKG-INFO +6 -6
- {httpdbg-1.1.2 → httpdbg-1.2.1}/README.md +5 -5
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/__init__.py +1 -1
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/args.py +1 -1
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/all.py +19 -10
- httpdbg-1.2.1/httpdbg/hooks/fastapi.py +156 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/flask.py +29 -9
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/socket.py +43 -40
- httpdbg-1.2.1/httpdbg/hooks/starlette.py +58 -0
- httpdbg-1.2.1/httpdbg/hooks/uvicorn.py +129 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/initiator.py +4 -4
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/records.py +10 -1
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/api.py +1 -1
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg.egg-info/PKG-INFO +6 -6
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg.egg-info/SOURCES.txt +6 -2
- httpdbg-1.2.1/tests/test_server_hook_fastapi.py +115 -0
- httpdbg-1.1.2/tests/test_hook_flask.py → httpdbg-1.2.1/tests/test_server_hook_flask.py +8 -2
- {httpdbg-1.1.2 → httpdbg-1.2.1}/LICENSE +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/__main__.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/env.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/exception.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/__init__.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/aiohttp.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/external.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/generic.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/http.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/httpx.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/pytest.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/requests.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/template_sitecustomize.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/unittest.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/urllib3.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/hooks/utils.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/log.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/mode_console.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/mode_module.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/mode_script.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/preview.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/server.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/utils.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/__init__.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/app.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/api.js +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/column.css +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/configuration.css +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/configuration.js +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/detail.css +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/favicon.ico +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/filter.js +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/index.htm +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/main.js +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/mustache.min.js +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/render.js +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/style.css +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/table.css +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg.egg-info/dependency_links.txt +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg.egg-info/entry_points.txt +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg.egg-info/top_level.txt +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/pyproject.toml +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/setup.cfg +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_api.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_cli.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_context_manager.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_hook_aiohttp.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_hook_httpx.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_hook_pytest.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_hook_requests.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_hook_unittest.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_hook_urllib3.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_initiator.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_mode_console.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_mode_module_pytest.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_mode_script.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_multiprocess.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_preview.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_pyhttpdbg_server.py +0 -0
- {httpdbg-1.1.2 → httpdbg-1.2.1}/tests/test_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpdbg
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: A very simple tool to debug HTTP(S) client and server requests.
|
|
5
5
|
Author-email: cle-b <cle@tictac.pm>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -114,7 +114,6 @@ Press CTRL+C to quit
|
|
|
114
114
|
|
|
115
115
|

|
|
116
116
|
|
|
117
|
-
This feature is new and not fully implemented. See [limitations](#limitations) for more details.
|
|
118
117
|
|
|
119
118
|
## initiators and groups
|
|
120
119
|
|
|
@@ -146,7 +145,8 @@ The initiator is the low-level socket method that receives the data. The group i
|
|
|
146
145
|
|
|
147
146
|
| packages | status | initiator | group |
|
|
148
147
|
|----------------|-------------------------------------|-----------|-----------|
|
|
149
|
-
| flask | supported |
|
|
148
|
+
| flask | supported | - | yes |
|
|
149
|
+
| fastapi | supported | - | yes |
|
|
150
150
|
|
|
151
151
|
### supported test framework
|
|
152
152
|
|
|
@@ -154,8 +154,8 @@ The requests are grouped by tests.
|
|
|
154
154
|
|
|
155
155
|
| packages | status | initiator | group |
|
|
156
156
|
|----------------|-------------------------------------|-----------|-----------|
|
|
157
|
-
| pytest | supported |
|
|
158
|
-
| unittest | supported |
|
|
157
|
+
| pytest | supported | - | yes |
|
|
158
|
+
| unittest | supported | - | yes |
|
|
159
159
|
|
|
160
160
|
## configuration
|
|
161
161
|
|
|
@@ -218,7 +218,7 @@ The requests:
|
|
|
218
218
|
|
|
219
219
|
Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
|
|
220
220
|
|
|
221
|
-
Support for recording requests on the server side is new in `httpdbg 1.0.0
|
|
221
|
+
Support for recording requests on the server side is new in `httpdbg 1.0.0`. In some cases, like with |FastAPI| using |uvloop|, we had to implement a new mechanism to make request recording possible. This is still a work in progress as we aim to support more frameworks.
|
|
222
222
|
|
|
223
223
|
## documentation
|
|
224
224
|
|
|
@@ -87,7 +87,6 @@ Press CTRL+C to quit
|
|
|
87
87
|
|
|
88
88
|

|
|
89
89
|
|
|
90
|
-
This feature is new and not fully implemented. See [limitations](#limitations) for more details.
|
|
91
90
|
|
|
92
91
|
## initiators and groups
|
|
93
92
|
|
|
@@ -119,7 +118,8 @@ The initiator is the low-level socket method that receives the data. The group i
|
|
|
119
118
|
|
|
120
119
|
| packages | status | initiator | group |
|
|
121
120
|
|----------------|-------------------------------------|-----------|-----------|
|
|
122
|
-
| flask | supported |
|
|
121
|
+
| flask | supported | - | yes |
|
|
122
|
+
| fastapi | supported | - | yes |
|
|
123
123
|
|
|
124
124
|
### supported test framework
|
|
125
125
|
|
|
@@ -127,8 +127,8 @@ The requests are grouped by tests.
|
|
|
127
127
|
|
|
128
128
|
| packages | status | initiator | group |
|
|
129
129
|
|----------------|-------------------------------------|-----------|-----------|
|
|
130
|
-
| pytest | supported |
|
|
131
|
-
| unittest | supported |
|
|
130
|
+
| pytest | supported | - | yes |
|
|
131
|
+
| unittest | supported | - | yes |
|
|
132
132
|
|
|
133
133
|
## configuration
|
|
134
134
|
|
|
@@ -191,7 +191,7 @@ The requests:
|
|
|
191
191
|
|
|
192
192
|
Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
|
|
193
193
|
|
|
194
|
-
Support for recording requests on the server side is new in `httpdbg 1.0.0
|
|
194
|
+
Support for recording requests on the server side is new in `httpdbg 1.0.0`. In some cases, like with |FastAPI| using |uvloop|, we had to implement a new mechanism to make request recording possible. This is still a work in progress as we aim to support more frameworks.
|
|
195
195
|
|
|
196
196
|
## documentation
|
|
197
197
|
|
|
@@ -16,7 +16,7 @@ def read_args(args: List[str]) -> Tuple[argparse.Namespace, List[str]]:
|
|
|
16
16
|
break
|
|
17
17
|
|
|
18
18
|
parser = argparse.ArgumentParser(
|
|
19
|
-
description="
|
|
19
|
+
description="httpdbg - easily debug HTTP client and server requests"
|
|
20
20
|
)
|
|
21
21
|
parser.add_argument(
|
|
22
22
|
"--host",
|
|
@@ -7,15 +7,19 @@ from typing import Union
|
|
|
7
7
|
|
|
8
8
|
from httpdbg.hooks.aiohttp import hook_aiohttp
|
|
9
9
|
from httpdbg.hooks.external import watcher_external
|
|
10
|
+
from httpdbg.hooks.fastapi import hook_fastapi
|
|
11
|
+
from httpdbg.hooks.flask import hook_flask
|
|
10
12
|
from httpdbg.hooks.generic import hook_generic
|
|
11
13
|
from httpdbg.hooks.http import hook_http
|
|
12
14
|
from httpdbg.hooks.httpx import hook_httpx
|
|
13
15
|
from httpdbg.hooks.pytest import hook_pytest
|
|
14
16
|
from httpdbg.hooks.requests import hook_requests
|
|
15
17
|
from httpdbg.hooks.socket import hook_socket
|
|
18
|
+
from httpdbg.hooks.starlette import hook_starlette
|
|
16
19
|
from httpdbg.hooks.unittest import hook_unittest
|
|
17
20
|
from httpdbg.hooks.urllib3 import hook_urllib3
|
|
18
|
-
from httpdbg.hooks.
|
|
21
|
+
from httpdbg.hooks.uvicorn import hook_uvicorn
|
|
22
|
+
|
|
19
23
|
from httpdbg.records import HTTPRecords
|
|
20
24
|
|
|
21
25
|
|
|
@@ -33,12 +37,17 @@ def httprecord(
|
|
|
33
37
|
with watcher_external(records, initiators, server):
|
|
34
38
|
with hook_flask(records):
|
|
35
39
|
with hook_socket(records):
|
|
36
|
-
with
|
|
37
|
-
with
|
|
38
|
-
with
|
|
39
|
-
with
|
|
40
|
-
with
|
|
41
|
-
with
|
|
42
|
-
with
|
|
43
|
-
with
|
|
44
|
-
|
|
40
|
+
with hook_fastapi(records):
|
|
41
|
+
with hook_starlette(records):
|
|
42
|
+
with hook_uvicorn(records):
|
|
43
|
+
with hook_http(records):
|
|
44
|
+
with hook_httpx(records):
|
|
45
|
+
with hook_requests(records):
|
|
46
|
+
with hook_urllib3(records):
|
|
47
|
+
with hook_aiohttp(records):
|
|
48
|
+
with hook_pytest(records):
|
|
49
|
+
with hook_unittest(records):
|
|
50
|
+
with hook_generic(
|
|
51
|
+
records, initiators
|
|
52
|
+
):
|
|
53
|
+
yield records
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
import functools
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import Generator
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
|
|
12
|
+
from httpdbg.hooks.utils import decorate
|
|
13
|
+
from httpdbg.hooks.utils import getcallargs
|
|
14
|
+
from httpdbg.hooks.utils import undecorate
|
|
15
|
+
from httpdbg.initiator import httpdbg_endpoint
|
|
16
|
+
from httpdbg.records import HTTPRecords
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# decorate the function decorated by @app.get; @app.post etc.
|
|
20
|
+
def set_hook_fastapi_endpoint(records: HTTPRecords, method: Callable):
|
|
21
|
+
|
|
22
|
+
@wraps(method)
|
|
23
|
+
def hook(*args, **kwargs):
|
|
24
|
+
|
|
25
|
+
with httpdbg_endpoint(
|
|
26
|
+
records,
|
|
27
|
+
method,
|
|
28
|
+
*args,
|
|
29
|
+
**kwargs,
|
|
30
|
+
):
|
|
31
|
+
ret = method(*args, **kwargs)
|
|
32
|
+
return ret
|
|
33
|
+
|
|
34
|
+
return hook
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def set_hook_fastapi_apirouter_add_api_route(
|
|
38
|
+
records: HTTPRecords,
|
|
39
|
+
method: Callable,
|
|
40
|
+
already_mapped: Union[Dict[Callable, Callable], None] = None,
|
|
41
|
+
):
|
|
42
|
+
|
|
43
|
+
@wraps(method)
|
|
44
|
+
def hook(*args, **kwargs):
|
|
45
|
+
callargs = getcallargs(method, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
param = "endpoint"
|
|
48
|
+
|
|
49
|
+
if param in callargs:
|
|
50
|
+
original_view_func = callargs[param]
|
|
51
|
+
if original_view_func not in already_mapped:
|
|
52
|
+
callargs[param] = decorate(
|
|
53
|
+
records, callargs[param], set_hook_fastapi_endpoint
|
|
54
|
+
)
|
|
55
|
+
already_mapped[original_view_func] = callargs[param]
|
|
56
|
+
else:
|
|
57
|
+
callargs[param] = already_mapped[original_view_func]
|
|
58
|
+
|
|
59
|
+
args = [callargs[param] if x == original_view_func else x for x in args]
|
|
60
|
+
if param in kwargs:
|
|
61
|
+
kwargs[param] = callargs[param]
|
|
62
|
+
|
|
63
|
+
return method(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
return hook
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# decorate the "app" returned by fastapi.routing.get_request_handler to link the socketdata and the endpoint group
|
|
69
|
+
def set_hook_fastapi_app(records: HTTPRecords, method: Callable):
|
|
70
|
+
|
|
71
|
+
@wraps(method)
|
|
72
|
+
async def hook(*args, **kwargs):
|
|
73
|
+
|
|
74
|
+
socketdata_id = None
|
|
75
|
+
|
|
76
|
+
callargs = getcallargs(method, *args, **kwargs)
|
|
77
|
+
|
|
78
|
+
if "request" in callargs:
|
|
79
|
+
if hasattr(callargs["request"], "__httpdbg_socketdata_id"):
|
|
80
|
+
socketdata_id = callargs["request"].__httpdbg_socketdata_id
|
|
81
|
+
|
|
82
|
+
with httpdbg_endpoint(
|
|
83
|
+
records,
|
|
84
|
+
method,
|
|
85
|
+
*args,
|
|
86
|
+
**kwargs,
|
|
87
|
+
) as group:
|
|
88
|
+
if group and (group.id in records.groups):
|
|
89
|
+
if socketdata_id and (socketdata_id in records._sockets):
|
|
90
|
+
records.groups[group.id].tbegin = records._sockets[
|
|
91
|
+
socketdata_id
|
|
92
|
+
].record.tbegin - datetime.timedelta(milliseconds=1)
|
|
93
|
+
records._sockets[socketdata_id].record.group_id = group.id
|
|
94
|
+
|
|
95
|
+
ret = await method(*args, **kwargs)
|
|
96
|
+
|
|
97
|
+
return ret
|
|
98
|
+
|
|
99
|
+
return hook
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def set_hook_fastapi_routing_get_request_handler(
|
|
103
|
+
records: HTTPRecords, method: Callable
|
|
104
|
+
):
|
|
105
|
+
|
|
106
|
+
@wraps(method)
|
|
107
|
+
def hook(*args, **kwargs):
|
|
108
|
+
|
|
109
|
+
app = method(*args, **kwargs)
|
|
110
|
+
|
|
111
|
+
app = decorate(records, app, set_hook_fastapi_app)
|
|
112
|
+
|
|
113
|
+
return app
|
|
114
|
+
|
|
115
|
+
return hook
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@contextmanager
|
|
119
|
+
def hook_fastapi(records: HTTPRecords) -> Generator[None, None, None]:
|
|
120
|
+
hooks = False
|
|
121
|
+
try:
|
|
122
|
+
import fastapi.routing
|
|
123
|
+
|
|
124
|
+
already_mapped: Dict[Callable, Callable] = {}
|
|
125
|
+
|
|
126
|
+
set_hook_fastapi_apirouter_add_api_route_with_already_mapped = (
|
|
127
|
+
functools.partial(
|
|
128
|
+
set_hook_fastapi_apirouter_add_api_route, already_mapped=already_mapped
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
fastapi.routing.APIRouter.add_api_route = decorate(
|
|
133
|
+
records,
|
|
134
|
+
fastapi.routing.APIRouter.add_api_route,
|
|
135
|
+
set_hook_fastapi_apirouter_add_api_route_with_already_mapped,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
fastapi.routing.get_request_handler = decorate(
|
|
139
|
+
records,
|
|
140
|
+
fastapi.routing.get_request_handler,
|
|
141
|
+
set_hook_fastapi_routing_get_request_handler,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
hooks = True
|
|
145
|
+
except ImportError:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
yield
|
|
149
|
+
|
|
150
|
+
if hooks:
|
|
151
|
+
fastapi.routing.APIRouter.add_api_route = undecorate(
|
|
152
|
+
fastapi.routing.APIRouter.add_api_route
|
|
153
|
+
)
|
|
154
|
+
fastapi.routing.get_request_handler = undecorate(
|
|
155
|
+
fastapi.routing.get_request_handler
|
|
156
|
+
)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
from collections.abc import Callable
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
+
import functools
|
|
4
5
|
from functools import wraps
|
|
6
|
+
from typing import Dict
|
|
5
7
|
from typing import Generator
|
|
8
|
+
from typing import Union
|
|
6
9
|
|
|
7
10
|
from httpdbg.hooks.utils import getcallargs
|
|
8
11
|
from httpdbg.hooks.utils import decorate
|
|
@@ -30,12 +33,11 @@ def set_hook_flask_endpoint(records: HTTPRecords, method: Callable):
|
|
|
30
33
|
return hook
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def set_hook_flask_add_url_rule(records: HTTPRecords, method: Callable):
|
|
36
|
+
def set_hook_flask_add_url_rule(
|
|
37
|
+
records: HTTPRecords,
|
|
38
|
+
method: Callable,
|
|
39
|
+
already_mapped: Union[Dict[Callable, Callable], None] = None,
|
|
40
|
+
):
|
|
39
41
|
|
|
40
42
|
def hook(*args, **kwargs):
|
|
41
43
|
|
|
@@ -62,7 +64,11 @@ def set_hook_flask_add_url_rule(records: HTTPRecords, method: Callable):
|
|
|
62
64
|
return hook
|
|
63
65
|
|
|
64
66
|
|
|
65
|
-
def set_hook_flask_register_error_handler(
|
|
67
|
+
def set_hook_flask_register_error_handler(
|
|
68
|
+
records: HTTPRecords,
|
|
69
|
+
method: Callable,
|
|
70
|
+
already_mapped: Union[Dict[Callable, Callable], None] = None,
|
|
71
|
+
):
|
|
66
72
|
|
|
67
73
|
def hook(*args, **kwargs):
|
|
68
74
|
|
|
@@ -95,14 +101,28 @@ def hook_flask(records: HTTPRecords) -> Generator[None, None, None]:
|
|
|
95
101
|
try:
|
|
96
102
|
import flask
|
|
97
103
|
|
|
104
|
+
# we must not apply the hook more than once on a mapped endpoint function
|
|
105
|
+
# AssertionError: View function mapping is overwriting an existing endpoint function: xxxx
|
|
106
|
+
already_mapped: Dict[Callable, Callable] = {}
|
|
107
|
+
|
|
108
|
+
set_hook_flask_add_url_rule_with_already_mapped = functools.partial(
|
|
109
|
+
set_hook_flask_add_url_rule, already_mapped=already_mapped
|
|
110
|
+
)
|
|
111
|
+
|
|
98
112
|
flask.Flask.add_url_rule = decorate(
|
|
99
|
-
records,
|
|
113
|
+
records,
|
|
114
|
+
flask.Flask.add_url_rule,
|
|
115
|
+
set_hook_flask_add_url_rule_with_already_mapped,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
set_hook_flask_register_error_handler_with_already_mapped = functools.partial(
|
|
119
|
+
set_hook_flask_register_error_handler, already_mapped=already_mapped
|
|
100
120
|
)
|
|
101
121
|
|
|
102
122
|
flask.Flask.register_error_handler = decorate(
|
|
103
123
|
records,
|
|
104
124
|
flask.Flask.register_error_handler,
|
|
105
|
-
|
|
125
|
+
set_hook_flask_register_error_handler_with_already_mapped,
|
|
106
126
|
)
|
|
107
127
|
flask.Flask.handle_exception = decorate(
|
|
108
128
|
records, flask.Flask.handle_exception, set_hook_flask_endpoint
|
|
@@ -186,50 +186,53 @@ def set_hook_for_socket_recv_into(records: HTTPRecords, method: Callable):
|
|
|
186
186
|
|
|
187
187
|
nbytes = method(self, buffer, *args, **kwargs)
|
|
188
188
|
|
|
189
|
-
if
|
|
190
|
-
if socketdata
|
|
191
|
-
|
|
192
|
-
f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
|
|
193
|
-
)
|
|
194
|
-
socketdata.record.receive_data(buffer[:nbytes])
|
|
195
|
-
else:
|
|
196
|
-
socketdata.rawdata += buffer[:nbytes]
|
|
197
|
-
http_detected = socketdata.http_detected()
|
|
198
|
-
if http_detected:
|
|
199
|
-
logger().info("RECV_INTO - http detected")
|
|
189
|
+
if buffer: # it appears that the buffer may be None (observed on Windows).
|
|
190
|
+
if socketdata:
|
|
191
|
+
if socketdata.record:
|
|
200
192
|
logger().info(
|
|
201
193
|
f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
|
|
202
194
|
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
) as initiator_and_group:
|
|
212
|
-
initiator, group, is_new = initiator_and_group
|
|
213
|
-
if is_new:
|
|
214
|
-
tbegin = socketdata.tbegin - datetime.timedelta(
|
|
215
|
-
milliseconds=1
|
|
216
|
-
)
|
|
217
|
-
initiator.tbegin = tbegin
|
|
218
|
-
group.tbegin = tbegin
|
|
219
|
-
socketdata.record = HTTPRecord(
|
|
220
|
-
records.current_initiator,
|
|
221
|
-
records.current_group,
|
|
222
|
-
records.current_tag,
|
|
223
|
-
tbegin=socketdata.tbegin,
|
|
224
|
-
is_client=False,
|
|
195
|
+
socketdata.record.receive_data(buffer[:nbytes])
|
|
196
|
+
else:
|
|
197
|
+
socketdata.rawdata += buffer[:nbytes]
|
|
198
|
+
http_detected = socketdata.http_detected()
|
|
199
|
+
if http_detected:
|
|
200
|
+
logger().info("RECV_INTO - http detected")
|
|
201
|
+
logger().info(
|
|
202
|
+
f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
|
|
225
203
|
)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
204
|
+
with httpdbg_initiator(
|
|
205
|
+
records,
|
|
206
|
+
traceback.extract_stack(),
|
|
207
|
+
method,
|
|
208
|
+
self,
|
|
209
|
+
buffer,
|
|
210
|
+
*args,
|
|
211
|
+
**kwargs,
|
|
212
|
+
) as initiator_and_group:
|
|
213
|
+
initiator, group, is_new = initiator_and_group
|
|
214
|
+
if is_new:
|
|
215
|
+
tbegin = socketdata.tbegin - datetime.timedelta(
|
|
216
|
+
milliseconds=1
|
|
217
|
+
)
|
|
218
|
+
initiator.tbegin = tbegin
|
|
219
|
+
group.tbegin = tbegin
|
|
220
|
+
socketdata.record = HTTPRecord(
|
|
221
|
+
records.current_initiator,
|
|
222
|
+
records.current_group,
|
|
223
|
+
records.current_tag,
|
|
224
|
+
tbegin=socketdata.tbegin,
|
|
225
|
+
is_client=False,
|
|
226
|
+
)
|
|
227
|
+
socketdata.record.address = socketdata.address
|
|
228
|
+
socketdata.record.ssl = socketdata.ssl
|
|
229
|
+
socketdata.record.receive_data(socketdata.rawdata)
|
|
230
|
+
if records.server:
|
|
231
|
+
records.requests[socketdata.record.id] = (
|
|
232
|
+
socketdata.record
|
|
233
|
+
)
|
|
234
|
+
elif http_detected is False: # if None, there is nothing to do
|
|
235
|
+
records._sockets[id(self)] = None
|
|
233
236
|
|
|
234
237
|
return nbytes
|
|
235
238
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Generator
|
|
6
|
+
|
|
7
|
+
from httpdbg.hooks.utils import decorate
|
|
8
|
+
from httpdbg.hooks.utils import getcallargs
|
|
9
|
+
from httpdbg.hooks.utils import undecorate
|
|
10
|
+
from httpdbg.records import HTTPRecords
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_hook_starlette_request_init(records: HTTPRecords, method: Callable):
|
|
14
|
+
|
|
15
|
+
@wraps(method)
|
|
16
|
+
def hook(self, *args, **kwargs):
|
|
17
|
+
|
|
18
|
+
callargs = getcallargs(method, self, *args, **kwargs)
|
|
19
|
+
|
|
20
|
+
if "receive" in callargs:
|
|
21
|
+
if hasattr(callargs["receive"], "__self__"):
|
|
22
|
+
if hasattr(callargs["receive"].__self__, "transport"):
|
|
23
|
+
if hasattr(
|
|
24
|
+
callargs["receive"].__self__.transport,
|
|
25
|
+
"_TCPTransport__httpdbg_socketdata",
|
|
26
|
+
):
|
|
27
|
+
# make the link between starlette.requests.Request (self) and uvicorn.protocols.http.httptools_impl.HttpToolsProtocol (socketdata)
|
|
28
|
+
self.__httpdbg_socketdata_id = callargs[
|
|
29
|
+
"receive"
|
|
30
|
+
].__self__.transport._TCPTransport__httpdbg_socketdata.id
|
|
31
|
+
|
|
32
|
+
return method(self, *args, **kwargs)
|
|
33
|
+
|
|
34
|
+
return hook
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def hook_starlette(records: HTTPRecords) -> Generator[None, None, None]:
|
|
39
|
+
hooks = False
|
|
40
|
+
try:
|
|
41
|
+
import starlette.requests
|
|
42
|
+
|
|
43
|
+
starlette.requests.Request.__init__ = decorate(
|
|
44
|
+
records,
|
|
45
|
+
starlette.requests.Request.__init__,
|
|
46
|
+
set_hook_starlette_request_init,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
hooks = True
|
|
50
|
+
except ImportError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
yield
|
|
54
|
+
|
|
55
|
+
if hooks:
|
|
56
|
+
starlette.requests.Request.__init__ = undecorate(
|
|
57
|
+
starlette.requests.Request.__init__
|
|
58
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
import datetime
|
|
5
|
+
import traceback
|
|
6
|
+
from typing import Generator
|
|
7
|
+
|
|
8
|
+
from httpdbg.hooks.utils import decorate
|
|
9
|
+
from httpdbg.hooks.utils import undecorate
|
|
10
|
+
from httpdbg.initiator import httpdbg_initiator
|
|
11
|
+
from httpdbg.log import logger
|
|
12
|
+
from httpdbg.records import HTTPRecord
|
|
13
|
+
from httpdbg.records import HTTPRecords
|
|
14
|
+
from httpdbg.records import SocketRawData
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# we use a proxy class to be able to override the write method
|
|
18
|
+
class TCPTransport:
|
|
19
|
+
def __init__(self, transport, socketdata: SocketRawData):
|
|
20
|
+
self.__httpdbg_original_transport = transport
|
|
21
|
+
self.__httpdbg_socketdata = socketdata
|
|
22
|
+
|
|
23
|
+
def write(self, buf):
|
|
24
|
+
if self.__httpdbg_socketdata:
|
|
25
|
+
if self.__httpdbg_socketdata.record:
|
|
26
|
+
self.__httpdbg_socketdata.record.send_data(buf)
|
|
27
|
+
return self.__httpdbg_original_transport.write(buf)
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, attr):
|
|
30
|
+
return getattr(self.__httpdbg_original_transport, attr)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_hook_uvicorn_connection_made(records: HTTPRecords, method: Callable):
|
|
34
|
+
|
|
35
|
+
def hook(self, transport):
|
|
36
|
+
|
|
37
|
+
socketdata = records.get_socket_data(self, force_new=True, is_uvicorn=True)
|
|
38
|
+
logger().debug(f"UVICORN - connection made - {socketdata}")
|
|
39
|
+
|
|
40
|
+
return method(self, TCPTransport(transport, socketdata))
|
|
41
|
+
|
|
42
|
+
return hook
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def set_hook_uvicorn_data_received(records: HTTPRecords, method: Callable):
|
|
46
|
+
|
|
47
|
+
def hook(self, data):
|
|
48
|
+
|
|
49
|
+
socketdata = records.get_socket_data(self)
|
|
50
|
+
if socketdata:
|
|
51
|
+
if socketdata.record:
|
|
52
|
+
socketdata.record.receive_data(data)
|
|
53
|
+
else:
|
|
54
|
+
socketdata.rawdata += data
|
|
55
|
+
http_detected = socketdata.http_detected()
|
|
56
|
+
if http_detected:
|
|
57
|
+
logger().info("UVICORN - http detected")
|
|
58
|
+
with httpdbg_initiator(
|
|
59
|
+
records,
|
|
60
|
+
traceback.extract_stack(),
|
|
61
|
+
method,
|
|
62
|
+
self,
|
|
63
|
+
data,
|
|
64
|
+
) as initiator_and_group:
|
|
65
|
+
initiator, group, is_new = initiator_and_group
|
|
66
|
+
if is_new:
|
|
67
|
+
tbegin = socketdata.tbegin - datetime.timedelta(
|
|
68
|
+
milliseconds=1
|
|
69
|
+
)
|
|
70
|
+
initiator.tbegin = tbegin
|
|
71
|
+
group.tbegin = tbegin
|
|
72
|
+
socketdata.record = HTTPRecord(
|
|
73
|
+
initiator.id,
|
|
74
|
+
group.id,
|
|
75
|
+
tbegin=socketdata.tbegin,
|
|
76
|
+
is_client=False,
|
|
77
|
+
)
|
|
78
|
+
socketdata.record.address = socketdata.address
|
|
79
|
+
socketdata.record.ssl = socketdata.ssl
|
|
80
|
+
socketdata.record.receive_data(socketdata.rawdata)
|
|
81
|
+
if records.server:
|
|
82
|
+
records.requests[socketdata.record.id] = socketdata.record
|
|
83
|
+
elif http_detected is False: # if None, there is nothing to do
|
|
84
|
+
records._sockets[id(self)] = None
|
|
85
|
+
|
|
86
|
+
return method(self, data)
|
|
87
|
+
|
|
88
|
+
return hook
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@contextmanager
|
|
92
|
+
def hook_uvicorn(records: HTTPRecords) -> Generator[None, None, None]:
|
|
93
|
+
hooks = False
|
|
94
|
+
try:
|
|
95
|
+
import uvicorn.protocols.http.httptools_impl
|
|
96
|
+
|
|
97
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.connection_made = (
|
|
98
|
+
decorate(
|
|
99
|
+
records,
|
|
100
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.connection_made,
|
|
101
|
+
set_hook_uvicorn_connection_made,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.data_received = (
|
|
106
|
+
decorate(
|
|
107
|
+
records,
|
|
108
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.data_received,
|
|
109
|
+
set_hook_uvicorn_data_received,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
hooks = True
|
|
114
|
+
except ImportError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
yield
|
|
118
|
+
|
|
119
|
+
if hooks:
|
|
120
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.connection_made = (
|
|
121
|
+
undecorate(
|
|
122
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.connection_made
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.data_received = (
|
|
126
|
+
undecorate(
|
|
127
|
+
uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.data_received
|
|
128
|
+
)
|
|
129
|
+
)
|
|
@@ -341,7 +341,7 @@ def httpdbg_group(
|
|
|
341
341
|
@contextmanager
|
|
342
342
|
def httpdbg_endpoint(
|
|
343
343
|
records: "HTTPRecords", original_method: Callable, *args, **kwargs
|
|
344
|
-
) -> Generator[None, None, None]:
|
|
344
|
+
) -> Generator[Union[Group, None], None, None]:
|
|
345
345
|
|
|
346
346
|
filename = inspect.getsourcefile(original_method)
|
|
347
347
|
if filename:
|
|
@@ -355,7 +355,7 @@ def httpdbg_endpoint(
|
|
|
355
355
|
original_method, *args, **kwargs
|
|
356
356
|
)
|
|
357
357
|
|
|
358
|
-
with httpdbg_group(records, instruction, full_label, update=True):
|
|
359
|
-
yield
|
|
358
|
+
with httpdbg_group(records, instruction, full_label, update=True) as group:
|
|
359
|
+
yield group
|
|
360
360
|
else:
|
|
361
|
-
yield
|
|
361
|
+
yield None
|
|
@@ -353,7 +353,7 @@ class HTTPRecords:
|
|
|
353
353
|
return len(self.requests)
|
|
354
354
|
|
|
355
355
|
def get_socket_data(
|
|
356
|
-
self, obj, extra_sock=None, force_new=False, request=None
|
|
356
|
+
self, obj, extra_sock=None, force_new=False, request=None, is_uvicorn=False
|
|
357
357
|
) -> Union[SocketRawData, None]:
|
|
358
358
|
"""Record a new SocketRawData (or get an existing one) and return it."""
|
|
359
359
|
socketdata = None
|
|
@@ -397,6 +397,9 @@ class HTTPRecords:
|
|
|
397
397
|
# only for async HTTP requests (not HTTPS) on Windows
|
|
398
398
|
self._sockets[id(obj)] = SocketRawData(id(obj), ("", 0), False)
|
|
399
399
|
socketdata = self._sockets[id(obj)]
|
|
400
|
+
elif is_uvicorn:
|
|
401
|
+
self._sockets[id(obj)] = SocketRawData(id(obj), ("", 0), False)
|
|
402
|
+
socketdata = self._sockets[id(obj)]
|
|
400
403
|
else:
|
|
401
404
|
if extra_sock:
|
|
402
405
|
try:
|
|
@@ -453,3 +456,9 @@ class HTTPRecords:
|
|
|
453
456
|
def add_group(self, group: Group):
|
|
454
457
|
self.groups[group.id] = group
|
|
455
458
|
self.current_group = group.id
|
|
459
|
+
|
|
460
|
+
def _print_for_debug(self):
|
|
461
|
+
for request in self.requests.values():
|
|
462
|
+
print(f"+ {request.url}")
|
|
463
|
+
print(f" - initiator: {self.initiators[request.initiator_id].label}")
|
|
464
|
+
print(f" - group: {self.groups[request.group_id].label}")
|
|
@@ -102,7 +102,7 @@ class RequestListPayload(JSONEncoder):
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
for id, initiator in records.initiators.items():
|
|
105
|
-
payload["initiators"][id] = initiator.to_json()
|
|
105
|
+
payload["initiators"][id] = initiator.to_json(full=True)
|
|
106
106
|
|
|
107
107
|
for id, group in records.groups.items():
|
|
108
108
|
payload["groups"][id] = group.to_json()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpdbg
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: A very simple tool to debug HTTP(S) client and server requests.
|
|
5
5
|
Author-email: cle-b <cle@tictac.pm>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -114,7 +114,6 @@ Press CTRL+C to quit
|
|
|
114
114
|
|
|
115
115
|

|
|
116
116
|
|
|
117
|
-
This feature is new and not fully implemented. See [limitations](#limitations) for more details.
|
|
118
117
|
|
|
119
118
|
## initiators and groups
|
|
120
119
|
|
|
@@ -146,7 +145,8 @@ The initiator is the low-level socket method that receives the data. The group i
|
|
|
146
145
|
|
|
147
146
|
| packages | status | initiator | group |
|
|
148
147
|
|----------------|-------------------------------------|-----------|-----------|
|
|
149
|
-
| flask | supported |
|
|
148
|
+
| flask | supported | - | yes |
|
|
149
|
+
| fastapi | supported | - | yes |
|
|
150
150
|
|
|
151
151
|
### supported test framework
|
|
152
152
|
|
|
@@ -154,8 +154,8 @@ The requests are grouped by tests.
|
|
|
154
154
|
|
|
155
155
|
| packages | status | initiator | group |
|
|
156
156
|
|----------------|-------------------------------------|-----------|-----------|
|
|
157
|
-
| pytest | supported |
|
|
158
|
-
| unittest | supported |
|
|
157
|
+
| pytest | supported | - | yes |
|
|
158
|
+
| unittest | supported | - | yes |
|
|
159
159
|
|
|
160
160
|
## configuration
|
|
161
161
|
|
|
@@ -218,7 +218,7 @@ The requests:
|
|
|
218
218
|
|
|
219
219
|
Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
|
|
220
220
|
|
|
221
|
-
Support for recording requests on the server side is new in `httpdbg 1.0.0
|
|
221
|
+
Support for recording requests on the server side is new in `httpdbg 1.0.0`. In some cases, like with |FastAPI| using |uvloop|, we had to implement a new mechanism to make request recording possible. This is still a work in progress as we aim to support more frameworks.
|
|
222
222
|
|
|
223
223
|
## documentation
|
|
224
224
|
|
|
@@ -25,6 +25,7 @@ httpdbg/hooks/__init__.py
|
|
|
25
25
|
httpdbg/hooks/aiohttp.py
|
|
26
26
|
httpdbg/hooks/all.py
|
|
27
27
|
httpdbg/hooks/external.py
|
|
28
|
+
httpdbg/hooks/fastapi.py
|
|
28
29
|
httpdbg/hooks/flask.py
|
|
29
30
|
httpdbg/hooks/generic.py
|
|
30
31
|
httpdbg/hooks/http.py
|
|
@@ -32,10 +33,12 @@ httpdbg/hooks/httpx.py
|
|
|
32
33
|
httpdbg/hooks/pytest.py
|
|
33
34
|
httpdbg/hooks/requests.py
|
|
34
35
|
httpdbg/hooks/socket.py
|
|
36
|
+
httpdbg/hooks/starlette.py
|
|
35
37
|
httpdbg/hooks/template_sitecustomize.py
|
|
36
38
|
httpdbg/hooks/unittest.py
|
|
37
39
|
httpdbg/hooks/urllib3.py
|
|
38
40
|
httpdbg/hooks/utils.py
|
|
41
|
+
httpdbg/hooks/uvicorn.py
|
|
39
42
|
httpdbg/webapp/__init__.py
|
|
40
43
|
httpdbg/webapp/api.py
|
|
41
44
|
httpdbg/webapp/app.py
|
|
@@ -71,7 +74,6 @@ tests/test_api.py
|
|
|
71
74
|
tests/test_cli.py
|
|
72
75
|
tests/test_context_manager.py
|
|
73
76
|
tests/test_hook_aiohttp.py
|
|
74
|
-
tests/test_hook_flask.py
|
|
75
77
|
tests/test_hook_httpx.py
|
|
76
78
|
tests/test_hook_pytest.py
|
|
77
79
|
tests/test_hook_requests.py
|
|
@@ -84,4 +86,6 @@ tests/test_mode_script.py
|
|
|
84
86
|
tests/test_multiprocess.py
|
|
85
87
|
tests/test_preview.py
|
|
86
88
|
tests/test_pyhttpdbg_server.py
|
|
87
|
-
tests/test_server.py
|
|
89
|
+
tests/test_server.py
|
|
90
|
+
tests/test_server_hook_fastapi.py
|
|
91
|
+
tests/test_server_hook_flask.py
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import platform
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import requests
|
|
8
|
+
import uvicorn
|
|
9
|
+
|
|
10
|
+
from httpdbg.hooks.all import httprecord
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@contextlib.contextmanager
|
|
14
|
+
def fastapi_app(port):
|
|
15
|
+
try:
|
|
16
|
+
config = uvicorn.Config("tests.demo_fastapi:app", port=port, log_level="debug")
|
|
17
|
+
server = uvicorn.Server(config)
|
|
18
|
+
|
|
19
|
+
thread = threading.Thread(target=server.run)
|
|
20
|
+
thread.start()
|
|
21
|
+
|
|
22
|
+
time.sleep(5)
|
|
23
|
+
yield
|
|
24
|
+
finally:
|
|
25
|
+
server.should_exit = True
|
|
26
|
+
thread.join(timeout=5)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# For some reason I don't yet understand, it looks like the hook for fastapi.routing.get_request_handler
|
|
30
|
+
# is applied only once when starting multiple FastAPI apps sequentially.
|
|
31
|
+
@pytest.mark.xfail(
|
|
32
|
+
platform.system().lower() == "windows",
|
|
33
|
+
reason="on windows, the server requests are recorded twice for fastapi",
|
|
34
|
+
)
|
|
35
|
+
def test_fastapi_endpoint(httpdbg_port):
|
|
36
|
+
|
|
37
|
+
with httprecord(client=False, server=True) as records:
|
|
38
|
+
with fastapi_app(httpdbg_port):
|
|
39
|
+
requests.get(f"http://localhost:{httpdbg_port}/")
|
|
40
|
+
|
|
41
|
+
# endpoint no parameters
|
|
42
|
+
assert len(records) == 1, records._print_for_debug()
|
|
43
|
+
record = records[0]
|
|
44
|
+
group = records.groups[record.group_id]
|
|
45
|
+
|
|
46
|
+
assert record.is_client is False
|
|
47
|
+
assert record.response.content == b'"Hello, World!"'
|
|
48
|
+
assert group.label == '@app.get("/")'
|
|
49
|
+
assert "hello_world()" in group.full_label
|
|
50
|
+
|
|
51
|
+
records.reset()
|
|
52
|
+
|
|
53
|
+
# endpoint with parameters
|
|
54
|
+
|
|
55
|
+
requests.get(f"http://localhost:{httpdbg_port}/items/123")
|
|
56
|
+
|
|
57
|
+
assert len(records) == 1, records._print_for_debug()
|
|
58
|
+
record = records[0]
|
|
59
|
+
group = records.groups[record.group_id]
|
|
60
|
+
|
|
61
|
+
assert record.is_client is False
|
|
62
|
+
assert record.response.content == b'{"item_id":123,"q":null}'
|
|
63
|
+
assert group.label == '@app.get("/items/{item_id}")'
|
|
64
|
+
full_label = (
|
|
65
|
+
group.full_label.replace(" ", "").replace("\t", "").replace("\n", "")
|
|
66
|
+
)
|
|
67
|
+
assert "get_item(item_id=123,q=None,)" in full_label
|
|
68
|
+
|
|
69
|
+
records.reset()
|
|
70
|
+
|
|
71
|
+
# http error
|
|
72
|
+
requests.get(f"http://localhost:{httpdbg_port}/items/456")
|
|
73
|
+
|
|
74
|
+
assert len(records) == 1
|
|
75
|
+
record = records[0]
|
|
76
|
+
group = records.groups[record.group_id]
|
|
77
|
+
|
|
78
|
+
assert record.is_client is False
|
|
79
|
+
assert record.status_code == 456
|
|
80
|
+
assert record.response.content == b'{"detail":"custom exception"}'
|
|
81
|
+
assert group.label == '@app.get("/items/{item_id}")'
|
|
82
|
+
full_label = (
|
|
83
|
+
group.full_label.replace(" ", "").replace("\t", "").replace("\n", "")
|
|
84
|
+
)
|
|
85
|
+
assert "get_item(item_id=456,q=None,)" in full_label
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.xfail(
|
|
89
|
+
platform.system().lower() == "windows",
|
|
90
|
+
reason="[#188] on windows, the server requests are recorded twice for fastapi",
|
|
91
|
+
)
|
|
92
|
+
def test_fastapi_client_request_in_endpoint(httpdbg_port, httpbin):
|
|
93
|
+
|
|
94
|
+
with httprecord(
|
|
95
|
+
client=True,
|
|
96
|
+
server=True,
|
|
97
|
+
ignore=[
|
|
98
|
+
(httpbin.host, httpbin.port),
|
|
99
|
+
],
|
|
100
|
+
) as records:
|
|
101
|
+
with fastapi_app(httpdbg_port):
|
|
102
|
+
requests.post(
|
|
103
|
+
f"http://localhost:{httpdbg_port}/withclientrequest",
|
|
104
|
+
json={"port": httpbin.port},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
assert len(records) == 3 # 2 + 1
|
|
108
|
+
|
|
109
|
+
assert records[0].is_client is True # the client request to /withclientrequest
|
|
110
|
+
assert records[1].is_client is False # the same request, received by the server
|
|
111
|
+
assert records[2].is_client is True # the request made inside the endpoint method
|
|
112
|
+
assert records[0].group_id == records[1].group_id
|
|
113
|
+
assert records[1].group_id == records[2].group_id
|
|
114
|
+
|
|
115
|
+
assert records[2].url == f"http://localhost:{httpbin.port}/"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
-
import
|
|
2
|
+
import platform
|
|
3
3
|
import threading
|
|
4
|
+
import time
|
|
4
5
|
|
|
5
6
|
from flask import Flask
|
|
7
|
+
import pytest
|
|
6
8
|
import requests
|
|
7
9
|
|
|
8
10
|
from httpdbg.hooks.all import httprecord
|
|
@@ -118,6 +120,10 @@ def test_flask_not_found(httpdbg_port):
|
|
|
118
120
|
assert "not_found(e=" in full_label
|
|
119
121
|
|
|
120
122
|
|
|
123
|
+
@pytest.mark.xfail(
|
|
124
|
+
platform.system().lower() == "windows",
|
|
125
|
+
reason="[#189] on Windows, the group label is empty in case of Internal Server Error in a Flask endpoint",
|
|
126
|
+
)
|
|
121
127
|
def test_flask_internal_server_error(httpdbg_port):
|
|
122
128
|
|
|
123
129
|
with httprecord(client=False, server=True) as records:
|
|
@@ -131,7 +137,7 @@ def test_flask_internal_server_error(httpdbg_port):
|
|
|
131
137
|
assert record.is_client is False
|
|
132
138
|
assert record.status_code == 500
|
|
133
139
|
assert b"Internal Server Error" in record.response.content
|
|
134
|
-
assert "def handle_exception" in group.label
|
|
140
|
+
assert "def handle_exception" in group.label, records._print_for_debug()
|
|
135
141
|
assert "division by zero" in group.full_label
|
|
136
142
|
|
|
137
143
|
|
|
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
|
|
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
|
{httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{httpdbg-1.1.2 → httpdbg-1.2.1}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg
RENAMED
|
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
|
|
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
|