forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtimeout.py
More file actions
95 lines (85 loc) · 4.26 KB
/
Copy pathtimeout.py
File metadata and controls
95 lines (85 loc) · 4.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import JSONResponse, Response
from starlette.types import ASGIApp
from fastapi import Request
import asyncio
import os
import time
from typing import Dict, Mapping, Optional
class TimeoutMiddleware(BaseHTTPMiddleware):
def __init__(
self,
app: ASGIApp,
methods_timeout: Optional[Mapping[str, object]] = None,
paths_timeout: Optional[Mapping[str, object]] = None,
) -> None:
super().__init__(app)
self.default_timeout = self._get_timeout_from_env("HTTP_DEFAULT_TIMEOUT", default=2 * 60)
self.maximum_age_seconds = self._get_timeout_from_env("HTTP_MAXIMUM_AGE_SECONDS", default=5 * 60)
self.clock_skew_allowance = self._get_timeout_from_env("HTTP_CLOCK_SKEW_ALLOWANCE", default=5 * 60)
self.methods_timeout = self._parse_methods_timeout(methods_timeout or {})
self.paths_timeout = self._parse_paths_timeout(paths_timeout or {})
@staticmethod
def _get_timeout_from_env(env_var: str, default: float) -> float:
timeout = os.environ.get(env_var, default)
try:
return float(timeout)
except (TypeError, ValueError):
raise ValueError(f"Invalid timeout value in env {env_var}: {timeout}")
@staticmethod
def _parse_methods_timeout(methods_timeout: Mapping[str, object]) -> Dict[str, float]:
result: Dict[str, float] = {}
for method, timeout in methods_timeout.items():
if timeout is None:
continue
try:
result[method.upper()] = float(timeout) # type: ignore[arg-type] # guarded by try/except
except (TypeError, ValueError):
raise ValueError(f"Invalid timeout value for method {method}: {timeout}")
return result
@staticmethod
def _parse_paths_timeout(paths_timeout: Mapping[str, object]) -> Dict[str, float]:
result: Dict[str, float] = {}
for path, timeout in paths_timeout.items():
if timeout is None:
continue
try:
result[path] = float(timeout) # type: ignore[arg-type] # guarded by try/except
except (TypeError, ValueError):
raise ValueError(f"Invalid timeout value for path {path}: {timeout}")
return result
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
# Check for stale request header first
# Uses clock_skew_allowance to tolerate client/server clock drift (#5929)
request_start_header = request.headers.get("x-request-start-time")
if request_start_header:
try:
request_start_time = float(request_start_header)
current_time = time.time()
request_age = current_time - request_start_time
if request_age > self.maximum_age_seconds + self.clock_skew_allowance:
return JSONResponse(
status_code=408,
content={
"error": "clock_skew",
"message": "Request rejected — your device clock may be out of sync",
"server_time": current_time,
"client_time": request_start_time,
"skew_seconds": round(request_age, 1),
"hint": "Check your device date/time settings and enable automatic time",
},
)
except (ValueError, TypeError):
pass
path_timeout = self.paths_timeout.get(request.url.path)
timeout = (
path_timeout if path_timeout is not None else self.methods_timeout.get(request.method, self.default_timeout)
)
# Stamp the monotonic request start so request-scoped read budgets
# (utils.other.list_budget) can derive an internal deadline that leaves
# serialization headroom under this middleware's hard cutoff.
request.state.omi_request_started_monotonic = time.monotonic()
try:
return await asyncio.wait_for(call_next(request), timeout=timeout)
except asyncio.TimeoutError:
return Response(status_code=504)