diff --git a/zerver/lib/request.py b/zerver/lib/request.py index 9f1dc0ac41..9f3f747fd2 100644 --- a/zerver/lib/request.py +++ b/zerver/lib/request.py @@ -1,5 +1,7 @@ import threading +import weakref from collections import defaultdict +from dataclasses import dataclass, field from functools import wraps from types import FunctionType from typing import ( @@ -8,6 +10,7 @@ from typing import ( Dict, Generic, List, + MutableMapping, Optional, Sequence, TypeVar, @@ -22,8 +25,54 @@ from django.http import HttpRequest, HttpResponse from django.utils.translation import gettext as _ from typing_extensions import Literal +import zerver.lib.rate_limiter as rate_limiter +import zerver.tornado.handlers as handlers from zerver.lib.exceptions import ErrorCode, InvalidJSONError, JsonableError from zerver.lib.types import Validator, ViewFuncT +from zerver.models import Client, Realm + + +@dataclass +class ZulipRequestNotes: + """This class contains extra metadata that Zulip associated with a + Django HttpRequest object. + + Note that most Optional fields will be definitely not None once + middlware has run. In the future, we may want to express that in + the types by having different types ZulipEarlyRequestNotes and + post-middleware ZulipRequestNotes types, but for now we have a lot + of `assert request_notes.foo is not None` when accessing them. + """ + + client: Optional[Client] = None + client_name: Optional[str] = None + client_version: Optional[str] = None + log_data: Optional[MutableMapping[str, Any]] = None + rate_limit: Optional[str] = None + requestor_for_logs: Optional[str] = None + # We use realm_cached to indicate whether the realm is cached or not. + # Because the default value of realm is None, which can indicate "unset" + # and "nonexistence" at the same time. + realm: Optional[Realm] = None + has_fetched_realm: bool = False + set_language: Optional[str] = None + ratelimits_applied: List["rate_limiter.RateLimitResult"] = field(default_factory=lambda: []) + query: Optional[str] = None + error_format: Optional[str] = None + placeholder_open_graph_description: Optional[str] = None + saved_response: Optional[HttpResponse] = None + tornado_handler: Optional["handlers.AsyncDjangoHandler"] = None + + +request_notes_map: MutableMapping[HttpRequest, ZulipRequestNotes] = weakref.WeakKeyDictionary() + + +def get_request_notes(request: HttpRequest) -> ZulipRequestNotes: + try: + return request_notes_map[request] + except KeyError: + request_notes_map[request] = ZulipRequestNotes() + return request_notes_map[request] class RequestConfusingParmsError(JsonableError): diff --git a/zerver/lib/validator.py b/zerver/lib/validator.py index 1811ae1dbe..32a63e35ac 100644 --- a/zerver/lib/validator.py +++ b/zerver/lib/validator.py @@ -49,9 +49,11 @@ from django.core.exceptions import ValidationError from django.core.validators import URLValidator, validate_email from django.utils.translation import gettext as _ -from zerver.lib.request import JsonableError, ResultT +from zerver.lib.exceptions import JsonableError from zerver.lib.types import ProfileFieldData, Validator +ResultT = TypeVar("ResultT") + def check_string(var_name: str, val: object) -> str: if not isinstance(val, str): diff --git a/zerver/tornado/handlers.py b/zerver/tornado/handlers.py index 306c44e024..e5325b8f18 100644 --- a/zerver/tornado/handlers.py +++ b/zerver/tornado/handlers.py @@ -13,7 +13,6 @@ from django.utils.cache import patch_vary_headers from tornado.wsgi import WSGIContainer from zerver.lib.response import json_response -from zerver.middleware import async_request_timer_restart, async_request_timer_stop from zerver.tornado.descriptors import get_descriptor_by_handler_id current_handler_id = 0 @@ -45,6 +44,10 @@ def finish_handler( ) -> None: err_msg = f"Got error finishing handler for queue {event_queue_id}" try: + # We import async_request_timer_restart during runtime + # to avoid cyclic dependency with zerver.lib.request + from zerver.middleware import async_request_timer_restart + # We call async_request_timer_restart here in case we are # being finished without any events (because another # get_events request has supplanted this request) @@ -143,6 +146,10 @@ class AsyncDjangoHandler(tornado.web.RequestHandler, base.BaseHandler): response = self.get_response(request) if hasattr(response, "asynchronous"): + # We import async_request_timer_restart during runtime + # to avoid cyclic dependency with zerver.lib.request + from zerver.middleware import async_request_timer_stop + # For asynchronous requests, this is where we exit # without returning the HttpResponse that Django # generated back to the user in order to long-poll the