django-request-replay 0.1.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.
Files changed (24) hide show
  1. django_request_replay-0.1.1/LICENSE +21 -0
  2. django_request_replay-0.1.1/PKG-INFO +203 -0
  3. django_request_replay-0.1.1/README.md +183 -0
  4. django_request_replay-0.1.1/django_request_replay/__init__.py +0 -0
  5. django_request_replay-0.1.1/django_request_replay/admin.py +17 -0
  6. django_request_replay-0.1.1/django_request_replay/apps.py +9 -0
  7. django_request_replay-0.1.1/django_request_replay/management/commands/__init__.py +0 -0
  8. django_request_replay-0.1.1/django_request_replay/management/commands/replay_request.py +53 -0
  9. django_request_replay-0.1.1/django_request_replay/management/commands/replay_requests.py +33 -0
  10. django_request_replay-0.1.1/django_request_replay/middleware.py +59 -0
  11. django_request_replay-0.1.1/django_request_replay/migrations/0001_initial.py +28 -0
  12. django_request_replay-0.1.1/django_request_replay/migrations/0002_replayrequest_body_truncated.py +18 -0
  13. django_request_replay-0.1.1/django_request_replay/migrations/__init__.py +0 -0
  14. django_request_replay-0.1.1/django_request_replay/models.py +17 -0
  15. django_request_replay-0.1.1/django_request_replay/redaction.py +57 -0
  16. django_request_replay-0.1.1/django_request_replay/replay.py +60 -0
  17. django_request_replay-0.1.1/django_request_replay/tests.py +493 -0
  18. django_request_replay-0.1.1/django_request_replay.egg-info/PKG-INFO +203 -0
  19. django_request_replay-0.1.1/django_request_replay.egg-info/SOURCES.txt +22 -0
  20. django_request_replay-0.1.1/django_request_replay.egg-info/dependency_links.txt +1 -0
  21. django_request_replay-0.1.1/django_request_replay.egg-info/requires.txt +2 -0
  22. django_request_replay-0.1.1/django_request_replay.egg-info/top_level.txt +1 -0
  23. django_request_replay-0.1.1/pyproject.toml +33 -0
  24. django_request_replay-0.1.1/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sedra Zeidan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-request-replay
3
+ Version: 0.1.1
4
+ Summary: Replay failed Django HTTP requests locally
5
+ Author: Sedra Zeidan
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Framework :: Django
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: Django<7.0,>=6.1
18
+ Requires-Dist: requests<3.0,>=2.32
19
+ Dynamic: license-file
20
+
21
+ # django-request-replay
22
+
23
+ Replay failed Django HTTP requests locally.
24
+
25
+ ## The Problem
26
+
27
+ Some Django errors are difficult to reproduce.
28
+
29
+ A request may fail in production or staging, but reproducing the exact request locally can be difficult.
30
+
31
+ `django-request-replay` captures failed requests and allows developers to replay them later.
32
+
33
+ ## How It Works
34
+
35
+ ```text
36
+ HTTP Request
37
+ ↓
38
+ Django Middleware
39
+ ↓
40
+ Request fails
41
+ ↓
42
+ Capture safely
43
+ ↓
44
+ ReplayRequest
45
+ ↓
46
+ Developer finds the request ID
47
+ ↓
48
+ python manage.py replay_request <id>
49
+ ↓
50
+ Request is replayed locally
51
+ Installation
52
+ Install the package with pip:
53
+ pip install django-request-replay
54
+ Add the app to INSTALLED_APPS:
55
+ INSTALLED_APPS = [
56
+ ...
57
+ "django_request_replay",
58
+ ]
59
+ Add the middleware:
60
+ MIDDLEWARE = [
61
+ "django_request_replay.middleware.RequestReplayMiddleware",
62
+ ...
63
+ ]
64
+ Run migrations:
65
+ python manage.py migrate
66
+ Configuration
67
+ Add the following settings to your Django project:
68
+ REPLAY_BASE_URL = "http://127.0.0.1:8000"
69
+ REPLAY_ENABLED = True
70
+ By default, the middleware captures:
71
+ 500
72
+ 502
73
+ 503
74
+ 504
75
+ You can customize the captured status codes:
76
+ REPLAY_STATUS_CODES = [500, 502, 503, 504]
77
+ The request body is limited to 10 MB by default:
78
+ MAX_BODY_SIZE = 10 * 1024 * 1024
79
+ Usage
80
+ List Captured Requests
81
+ To see captured failed requests:
82
+ python manage.py replay_requests
83
+ Example:
84
+ ID METHOD PATH STATUS
85
+ ------------------------------------------------
86
+ 12 POST /api/payment/ 500
87
+ 11 GET /api/profile/ 503
88
+ 10 POST /api/orders/ 500
89
+ You can filter by status code:
90
+ python manage.py replay_requests --status 500
91
+ Limit the number of results:
92
+ python manage.py replay_requests --limit 5
93
+ Combine filters:
94
+ python manage.py replay_requests --status 500 --limit 5
95
+ Replay a Request
96
+ Once you know the request ID:
97
+ python manage.py replay_request 12
98
+ Example:
99
+ Replay completed: POST /api/payment/ -> 500 (0.145s)
100
+ The request is sent again to the configured REPLAY_BASE_URL.
101
+ Verbose Mode
102
+ Use --verbose to see additional response information:
103
+ python manage.py replay_request 12 --verbose
104
+ Example:
105
+ Replay completed: POST /api/payment/ -> 500 (0.145s)
106
+
107
+ Response status: 500
108
+ Content-Type: text/html
109
+ Response size: 4218 bytes
110
+ Security
111
+ Sensitive request data is redacted before being stored.
112
+ Sensitive Headers
113
+ The following headers are redacted by default:
114
+ Authorization
115
+ Cookie
116
+ Set-Cookie
117
+ X-API-Key
118
+ Sensitive Body Fields
119
+ The following JSON fields are redacted:
120
+ password
121
+ token
122
+ secret
123
+ api_key
124
+ Example:
125
+ {
126
+ "username": "sedra",
127
+ "password": "[REDACTED]",
128
+ "token": "[REDACTED]"
129
+ }
130
+ Nested JSON objects and lists are also handled recursively.
131
+ Sensitive authentication headers are not sent during replay.
132
+ Large Request Bodies
133
+ Request bodies larger than the configured limit are not stored.
134
+ Instead, the request is marked as truncated:
135
+ body_truncated = True
136
+ This helps prevent the replay database from growing unexpectedly because of very large request bodies.
137
+ Replay Protection
138
+ Replayed requests include a special header:
139
+ X-Django-Replay: 1
140
+ The middleware detects this header and does not capture the replayed request again.
141
+ This prevents an infinite capture loop:
142
+ Replay request
143
+ ↓
144
+ Fails again
145
+ ↓
146
+ Captured again
147
+ ↓
148
+ Replayed again
149
+ ↓
150
+ ...
151
+ Testing
152
+ Run the test suite with:
153
+ python example/manage.py test django_request_replay
154
+ The project includes tests for:
155
+ Request redaction
156
+ Header redaction
157
+ Nested JSON redaction
158
+ Failed request capture
159
+ Exception capture
160
+ 5xx responses
161
+ Body truncation
162
+ Query parameters
163
+ Replay behavior
164
+ Sensitive headers
165
+ Replay loop prevention
166
+ Management commands
167
+ Disabled replay
168
+ Connection errors
169
+ Project Structure
170
+
171
+ django-request-replay/
172
+ │
173
+ ├── django_request_replay/
174
+ │ ├── middleware.py
175
+ │ ├── models.py
176
+ │ ├── redaction.py
177
+ │ ├── replay.py
178
+ │ ├── admin.py
179
+ │ ├── tests.py
180
+ │ └── management/
181
+ │ └── commands/
182
+ │ ├── replay_request.py
183
+ │ └── replay_requests.py
184
+ │
185
+ ├── example/
186
+ │ ├── manage.py
187
+ │ └── config/
188
+ │
189
+ ├── pyproject.toml
190
+ ├── README.md
191
+ └── .gitignore
192
+ Roadmap
193
+ [ ] Configurable redaction rules
194
+ [ ] Request retention and cleanup command
195
+ [ ] Better query parameter preservation
196
+ [ ] Structured logging
197
+ [ ] GitHub Actions CI
198
+ [ ] PyPI release
199
+ [ ] More Django version compatibility
200
+ [ ] Response comparison
201
+ [ ] Advanced replay options
202
+ License
203
+ MIT
@@ -0,0 +1,183 @@
1
+ # django-request-replay
2
+
3
+ Replay failed Django HTTP requests locally.
4
+
5
+ ## The Problem
6
+
7
+ Some Django errors are difficult to reproduce.
8
+
9
+ A request may fail in production or staging, but reproducing the exact request locally can be difficult.
10
+
11
+ `django-request-replay` captures failed requests and allows developers to replay them later.
12
+
13
+ ## How It Works
14
+
15
+ ```text
16
+ HTTP Request
17
+ ↓
18
+ Django Middleware
19
+ ↓
20
+ Request fails
21
+ ↓
22
+ Capture safely
23
+ ↓
24
+ ReplayRequest
25
+ ↓
26
+ Developer finds the request ID
27
+ ↓
28
+ python manage.py replay_request <id>
29
+ ↓
30
+ Request is replayed locally
31
+ Installation
32
+ Install the package with pip:
33
+ pip install django-request-replay
34
+ Add the app to INSTALLED_APPS:
35
+ INSTALLED_APPS = [
36
+ ...
37
+ "django_request_replay",
38
+ ]
39
+ Add the middleware:
40
+ MIDDLEWARE = [
41
+ "django_request_replay.middleware.RequestReplayMiddleware",
42
+ ...
43
+ ]
44
+ Run migrations:
45
+ python manage.py migrate
46
+ Configuration
47
+ Add the following settings to your Django project:
48
+ REPLAY_BASE_URL = "http://127.0.0.1:8000"
49
+ REPLAY_ENABLED = True
50
+ By default, the middleware captures:
51
+ 500
52
+ 502
53
+ 503
54
+ 504
55
+ You can customize the captured status codes:
56
+ REPLAY_STATUS_CODES = [500, 502, 503, 504]
57
+ The request body is limited to 10 MB by default:
58
+ MAX_BODY_SIZE = 10 * 1024 * 1024
59
+ Usage
60
+ List Captured Requests
61
+ To see captured failed requests:
62
+ python manage.py replay_requests
63
+ Example:
64
+ ID METHOD PATH STATUS
65
+ ------------------------------------------------
66
+ 12 POST /api/payment/ 500
67
+ 11 GET /api/profile/ 503
68
+ 10 POST /api/orders/ 500
69
+ You can filter by status code:
70
+ python manage.py replay_requests --status 500
71
+ Limit the number of results:
72
+ python manage.py replay_requests --limit 5
73
+ Combine filters:
74
+ python manage.py replay_requests --status 500 --limit 5
75
+ Replay a Request
76
+ Once you know the request ID:
77
+ python manage.py replay_request 12
78
+ Example:
79
+ Replay completed: POST /api/payment/ -> 500 (0.145s)
80
+ The request is sent again to the configured REPLAY_BASE_URL.
81
+ Verbose Mode
82
+ Use --verbose to see additional response information:
83
+ python manage.py replay_request 12 --verbose
84
+ Example:
85
+ Replay completed: POST /api/payment/ -> 500 (0.145s)
86
+
87
+ Response status: 500
88
+ Content-Type: text/html
89
+ Response size: 4218 bytes
90
+ Security
91
+ Sensitive request data is redacted before being stored.
92
+ Sensitive Headers
93
+ The following headers are redacted by default:
94
+ Authorization
95
+ Cookie
96
+ Set-Cookie
97
+ X-API-Key
98
+ Sensitive Body Fields
99
+ The following JSON fields are redacted:
100
+ password
101
+ token
102
+ secret
103
+ api_key
104
+ Example:
105
+ {
106
+ "username": "sedra",
107
+ "password": "[REDACTED]",
108
+ "token": "[REDACTED]"
109
+ }
110
+ Nested JSON objects and lists are also handled recursively.
111
+ Sensitive authentication headers are not sent during replay.
112
+ Large Request Bodies
113
+ Request bodies larger than the configured limit are not stored.
114
+ Instead, the request is marked as truncated:
115
+ body_truncated = True
116
+ This helps prevent the replay database from growing unexpectedly because of very large request bodies.
117
+ Replay Protection
118
+ Replayed requests include a special header:
119
+ X-Django-Replay: 1
120
+ The middleware detects this header and does not capture the replayed request again.
121
+ This prevents an infinite capture loop:
122
+ Replay request
123
+ ↓
124
+ Fails again
125
+ ↓
126
+ Captured again
127
+ ↓
128
+ Replayed again
129
+ ↓
130
+ ...
131
+ Testing
132
+ Run the test suite with:
133
+ python example/manage.py test django_request_replay
134
+ The project includes tests for:
135
+ Request redaction
136
+ Header redaction
137
+ Nested JSON redaction
138
+ Failed request capture
139
+ Exception capture
140
+ 5xx responses
141
+ Body truncation
142
+ Query parameters
143
+ Replay behavior
144
+ Sensitive headers
145
+ Replay loop prevention
146
+ Management commands
147
+ Disabled replay
148
+ Connection errors
149
+ Project Structure
150
+
151
+ django-request-replay/
152
+ │
153
+ ├── django_request_replay/
154
+ │ ├── middleware.py
155
+ │ ├── models.py
156
+ │ ├── redaction.py
157
+ │ ├── replay.py
158
+ │ ├── admin.py
159
+ │ ├── tests.py
160
+ │ └── management/
161
+ │ └── commands/
162
+ │ ├── replay_request.py
163
+ │ └── replay_requests.py
164
+ │
165
+ ├── example/
166
+ │ ├── manage.py
167
+ │ └── config/
168
+ │
169
+ ├── pyproject.toml
170
+ ├── README.md
171
+ └── .gitignore
172
+ Roadmap
173
+ [ ] Configurable redaction rules
174
+ [ ] Request retention and cleanup command
175
+ [ ] Better query parameter preservation
176
+ [ ] Structured logging
177
+ [ ] GitHub Actions CI
178
+ [ ] PyPI release
179
+ [ ] More Django version compatibility
180
+ [ ] Response comparison
181
+ [ ] Advanced replay options
182
+ License
183
+ MIT
@@ -0,0 +1,17 @@
1
+ from django.contrib import admin
2
+
3
+ from .models import ReplayRequest
4
+
5
+
6
+ @admin.register(ReplayRequest)
7
+ class ReplayRequestAdmin(admin.ModelAdmin):
8
+ list_display = (
9
+ "id",
10
+ "method",
11
+ "path",
12
+ "status_code",
13
+ "body_truncated",
14
+ "created_at",
15
+ )
16
+ list_filter = ("status_code", "body_truncated", "method")
17
+ search_fields = ("path", "method")
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class DjangoRequestReplayConfig(AppConfig):
7
+ default_auto_field = "django.db.models.BigAutoField"
8
+ name = "django_request_replay"
9
+ path = str(Path(__file__).resolve().parent)
@@ -0,0 +1,53 @@
1
+ from django.core.management.base import BaseCommand, CommandError
2
+ import requests
3
+ from django_request_replay.replay import replay_request
4
+ from django_request_replay.models import ReplayRequest
5
+
6
+
7
+ class Command(BaseCommand):
8
+ help = "Replay a captured Django request"
9
+
10
+ def add_arguments(self, parser):
11
+ parser.add_argument(
12
+ "replay_request_id",
13
+ type=int,
14
+ )
15
+ parser.add_argument(
16
+ "--verbose",
17
+ action="store_true",
18
+ help="Print detailed information about the replayed request and response",)
19
+
20
+ def handle(self, *args, **options):
21
+ replay_request_id = options["replay_request_id"]
22
+
23
+ if not ReplayRequest.objects.filter(
24
+ id=replay_request_id
25
+ ).exists():
26
+ raise CommandError(
27
+ f"ReplayRequest with id {replay_request_id} does not exist."
28
+ )
29
+ captured_request = ReplayRequest.objects.get(
30
+ id=replay_request_id
31
+ )
32
+ try:
33
+ response = replay_request(replay_request_id)
34
+ except requests.exceptions.ConnectionError:
35
+ raise CommandError(
36
+ "Could not connect to the replay server. "
37
+ "Make sure Django is running and REPLAY_BASE_URL is correct."
38
+ )
39
+
40
+ self.stdout.write(
41
+ self.style.SUCCESS(
42
+ f"Replay completed: "
43
+ f"{captured_request.method} "
44
+ f"{captured_request.path} "
45
+ f"-> {response.status_code} "
46
+ f"({response.replay_elapsed_time:.3f}s)"
47
+ )
48
+ )
49
+ if options["verbose"]:
50
+ self.stdout.write("")
51
+ self.stdout.write(f"Response status: {response.status_code}")
52
+ self.stdout.write(f"Content-Type: {response.headers.get('Content-Type')}")
53
+ self.stdout.write(f"Response size: {len(response.content)} bytes")
@@ -0,0 +1,33 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from django_request_replay.models import ReplayRequest
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = "List captured failed requests"
8
+
9
+ def handle(self, *args, **options):
10
+ requests = ReplayRequest.objects.order_by("-created_at")
11
+
12
+ if not requests.exists():
13
+ self.stdout.write("No captured requests found.")
14
+ return
15
+
16
+ self.stdout.write(
17
+ f"{'ID':<6}"
18
+ f"{'METHOD':<10}"
19
+ f"{'PATH':<40}"
20
+ f"{'STATUS':<10}"
21
+ f"CREATED"
22
+ )
23
+
24
+ self.stdout.write("-" * 90)
25
+
26
+ for request in requests:
27
+ self.stdout.write(
28
+ f"{request.id:<6}"
29
+ f"{request.method:<10}"
30
+ f"{request.path:<40}"
31
+ f"{request.status_code:<10}"
32
+ f"{request.created_at}"
33
+ )
@@ -0,0 +1,59 @@
1
+ from .models import ReplayRequest
2
+ from .redaction import redact_headers , redact_body
3
+ from django.conf import settings
4
+
5
+ class RequestReplayMiddleware:
6
+ def __init__(self, get_response):
7
+ self.get_response = get_response
8
+
9
+ self.status_codes = getattr(
10
+ settings,
11
+ "REPLAY_STATUS_CODES",
12
+ [500, 502, 503, 504],
13
+ )
14
+
15
+ self.max_body_size = getattr(
16
+ settings,
17
+ "MAX_BODY_SIZE",
18
+ 10 * 1024 * 1024,
19
+ )
20
+ def _get_body(self, request):
21
+ body = request.body
22
+ if len(body) > self.max_body_size:
23
+ return "", True
24
+ return redact_body(
25
+ body.decode("utf-8", errors="replace")
26
+ ), False
27
+ def _save_request(self, request, status_code):
28
+ body, body_truncated = self._get_body(request)
29
+
30
+ user = getattr(request, "user", None)
31
+
32
+ ReplayRequest.objects.create(
33
+ method=request.method,
34
+ path=request.path,
35
+ query_params=request.GET.dict(),
36
+ headers=redact_headers(request.headers),
37
+ body=body,
38
+ body_truncated=body_truncated,
39
+ status_code=status_code,
40
+ user_id=(
41
+ str(user.pk)
42
+ if user is not None and user.is_authenticated
43
+ else None
44
+ ),
45
+ )
46
+ def __call__(self, request):
47
+ if request.headers.get("X-Django-Replay") == "1":
48
+ return self.get_response(request)
49
+ try:
50
+ response = self.get_response(request)
51
+
52
+ except Exception:
53
+ self._save_request(request, 500)
54
+ raise
55
+
56
+ if response.status_code in self.status_codes:
57
+ self._save_request(request, response.status_code)
58
+ return response
59
+
@@ -0,0 +1,28 @@
1
+ # Generated by Django 6.1.1 on 2026-09-25 22:51
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ initial = True
9
+
10
+ dependencies = [
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='ReplayRequest',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('method', models.CharField(max_length=10)),
19
+ ('path', models.CharField(max_length=500)),
20
+ ('query_params', models.JSONField(default=dict)),
21
+ ('headers', models.JSONField(default=dict)),
22
+ ('body', models.TextField(blank=True)),
23
+ ('status_code', models.PositiveIntegerField()),
24
+ ('user_id', models.CharField(blank=True, max_length=255, null=True)),
25
+ ('created_at', models.DateTimeField(auto_now_add=True)),
26
+ ],
27
+ ),
28
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 6.1.1 on 2026-09-27 23:15
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('django_request_replay', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='replayrequest',
15
+ name='body_truncated',
16
+ field=models.BooleanField(default=False),
17
+ ),
18
+ ]
@@ -0,0 +1,17 @@
1
+ from django.db import models
2
+
3
+
4
+ class ReplayRequest(models.Model):
5
+ method = models.CharField(max_length=10)
6
+ path = models.CharField(max_length=500)
7
+ query_params = models.JSONField(default=dict)
8
+ headers = models.JSONField(default=dict)
9
+ body = models.TextField(blank=True)
10
+ body_truncated = models.BooleanField(default=False)
11
+ status_code = models.PositiveIntegerField()
12
+ user_id = models.CharField(
13
+ max_length=255,
14
+ null=True,
15
+ blank=True,
16
+ )
17
+ created_at = models.DateTimeField(auto_now_add=True)