Add request.id (#2005)
This commit is contained in:
parent
6c03dd87b1
commit
0d7e2f0d67
|
@ -34,6 +34,7 @@ DEFAULT_CONFIG = {
|
||||||
"REAL_IP_HEADER": None,
|
"REAL_IP_HEADER": None,
|
||||||
"PROXIES_COUNT": None,
|
"PROXIES_COUNT": None,
|
||||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
|
"REQUEST_ID_HEADER": "X-Request-ID",
|
||||||
"FALLBACK_ERROR_FORMAT": "html",
|
"FALLBACK_ERROR_FORMAT": "html",
|
||||||
"REGISTER": True,
|
"REGISTER": True,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import email.utils
|
import email.utils
|
||||||
|
import uuid
|
||||||
|
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
|
@ -51,6 +52,7 @@ class Request:
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"__weakref__",
|
"__weakref__",
|
||||||
"_cookies",
|
"_cookies",
|
||||||
|
"_id",
|
||||||
"_ip",
|
"_ip",
|
||||||
"_parsed_url",
|
"_parsed_url",
|
||||||
"_port",
|
"_port",
|
||||||
|
@ -82,6 +84,7 @@ class Request:
|
||||||
self.raw_url = url_bytes
|
self.raw_url = url_bytes
|
||||||
# TODO: Content-Encoding detection
|
# TODO: Content-Encoding detection
|
||||||
self._parsed_url = parse_url(url_bytes)
|
self._parsed_url = parse_url(url_bytes)
|
||||||
|
self._id = None
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
@ -110,6 +113,10 @@ class Request:
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
return f"<{class_name}: {self.method} {self.path}>"
|
return f"<{class_name}: {self.method} {self.path}>"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_id(*_):
|
||||||
|
return uuid.uuid4()
|
||||||
|
|
||||||
async def respond(
|
async def respond(
|
||||||
self, response=None, *, status=200, headers=None, content_type=None
|
self, response=None, *, status=200, headers=None, content_type=None
|
||||||
):
|
):
|
||||||
|
@ -148,6 +155,26 @@ class Request:
|
||||||
if not self.body:
|
if not self.body:
|
||||||
self.body = b"".join([data async for data in self.stream])
|
self.body = b"".join([data async for data in self.stream])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
if not self._id:
|
||||||
|
self._id = self.headers.get(
|
||||||
|
self.app.config.REQUEST_ID_HEADER,
|
||||||
|
self.__class__.generate_id(self),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try casting to a UUID or an integer
|
||||||
|
if isinstance(self._id, str):
|
||||||
|
try:
|
||||||
|
self._id = uuid.UUID(self._id)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
self._id = int(self._id)
|
||||||
|
except ValueError:
|
||||||
|
...
|
||||||
|
|
||||||
|
return self._id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if self.parsed_json is None:
|
if self.parsed_json is None:
|
||||||
|
|
76
tests/test_request.py
Normal file
76
tests/test_request.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
from unittest.mock import Mock
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic import Sanic, response
|
||||||
|
from sanic.request import Request, uuid
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_request_id_not_called(monkeypatch):
|
||||||
|
monkeypatch.setattr(uuid, "uuid4", Mock())
|
||||||
|
request = Request(b"/", {}, None, "GET", None, None)
|
||||||
|
|
||||||
|
assert request._id is None
|
||||||
|
uuid.uuid4.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_id_generates_from_request(monkeypatch):
|
||||||
|
monkeypatch.setattr(Request, "generate_id", Mock())
|
||||||
|
Request.generate_id.return_value = 1
|
||||||
|
request = Request(b"/", {}, None, "GET", None, Mock())
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
request.id
|
||||||
|
Request.generate_id.assert_called_once_with(request)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_id_defaults_uuid():
|
||||||
|
request = Request(b"/", {}, None, "GET", None, Mock())
|
||||||
|
|
||||||
|
assert isinstance(request.id, UUID)
|
||||||
|
|
||||||
|
# Makes sure that it has been cached and not called multiple times
|
||||||
|
assert request.id == request.id == request._id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"request_id,expected_type",
|
||||||
|
(
|
||||||
|
(99, int),
|
||||||
|
(uuid4(), UUID),
|
||||||
|
("foo", str),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_request_id(request_id, expected_type):
|
||||||
|
app = Sanic("req-generator")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get(request):
|
||||||
|
return response.empty()
|
||||||
|
|
||||||
|
request, _ = app.test_client.get(
|
||||||
|
"/", headers={"X-REQUEST-ID": f"{request_id}"}
|
||||||
|
)
|
||||||
|
assert request.id == request_id
|
||||||
|
assert type(request.id) == expected_type
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_generator():
|
||||||
|
REQUEST_ID = 99
|
||||||
|
|
||||||
|
class FooRequest(Request):
|
||||||
|
@classmethod
|
||||||
|
def generate_id(cls, request):
|
||||||
|
return int(request.headers["some-other-request-id"]) * 2
|
||||||
|
|
||||||
|
app = Sanic("req-generator", request_class=FooRequest)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get(request):
|
||||||
|
return response.empty()
|
||||||
|
|
||||||
|
request, _ = app.test_client.get(
|
||||||
|
"/", headers={"SOME-OTHER-REQUEST-ID": f"{REQUEST_ID}"}
|
||||||
|
)
|
||||||
|
assert request.id == REQUEST_ID * 2
|
Loading…
Reference in New Issue
Block a user