Add request.id (#2005)

This commit is contained in:
Adam Hopkins 2021-01-19 04:25:39 +02:00 committed by GitHub
parent 6c03dd87b1
commit 0d7e2f0d67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 104 additions and 0 deletions

View File

@ -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,
} }

View File

@ -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
View 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