Add request.id (#2005)
This commit is contained in:
		| @@ -34,6 +34,7 @@ DEFAULT_CONFIG = { | ||||
|     "REAL_IP_HEADER": None, | ||||
|     "PROXIES_COUNT": None, | ||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||
|     "REQUEST_ID_HEADER": "X-Request-ID", | ||||
|     "FALLBACK_ERROR_FORMAT": "html", | ||||
|     "REGISTER": True, | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import email.utils | ||||
| import uuid | ||||
|  | ||||
| from collections import defaultdict, namedtuple | ||||
| from http.cookies import SimpleCookie | ||||
| @@ -51,6 +52,7 @@ class Request: | ||||
|     __slots__ = ( | ||||
|         "__weakref__", | ||||
|         "_cookies", | ||||
|         "_id", | ||||
|         "_ip", | ||||
|         "_parsed_url", | ||||
|         "_port", | ||||
| @@ -82,6 +84,7 @@ class Request: | ||||
|         self.raw_url = url_bytes | ||||
|         # TODO: Content-Encoding detection | ||||
|         self._parsed_url = parse_url(url_bytes) | ||||
|         self._id = None | ||||
|         self.app = app | ||||
|  | ||||
|         self.headers = headers | ||||
| @@ -110,6 +113,10 @@ class Request: | ||||
|         class_name = self.__class__.__name__ | ||||
|         return f"<{class_name}: {self.method} {self.path}>" | ||||
|  | ||||
|     @classmethod | ||||
|     def generate_id(*_): | ||||
|         return uuid.uuid4() | ||||
|  | ||||
|     async def respond( | ||||
|         self, response=None, *, status=200, headers=None, content_type=None | ||||
|     ): | ||||
| @@ -148,6 +155,26 @@ class Request: | ||||
|         if not self.body: | ||||
|             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 | ||||
|     def json(self): | ||||
|         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 | ||||
		Reference in New Issue
	
	Block a user
	 Adam Hopkins
					Adam Hopkins