File size: 1,759 Bytes
d2ae3d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Server-owned request provenance, inherited by async output-recording tasks."""
from contextvars import ContextVar
from typing import Awaitable, Callable

from fastapi import Request, Response
from fastapi.routing import APIRoute

CLIENT_SOURCES = frozenset({'android', 'wechat', 'ios', 'web', 'unknown'})
current_client_source: ContextVar[str] = ContextVar('client_source', default='unknown')


def validate_client_source(source: str) -> str:
    if source not in CLIENT_SOURCES:
        raise ValueError('Unsupported client source')
    return source


def resolve_client_source(request: Request) -> str:
    """Explicit routes win; legacy shared Android routes carry x-device-id."""
    path = request.url.path.rstrip('/')
    for prefix, source in (('/facescore/app/', 'android'),
                           ('/facescore/wechat/', 'wechat'),
                           ('/facescore/ios/', 'ios'), ('/facescore/web/', 'web')):
        if path.startswith(prefix):
            return source
    # Original analyze is the mini-program entry; app/analyze delegates internally.
    if path == '/facescore/analyze':
        return 'wechat'
    if path.startswith('/facescore/'):
        return 'android' if request.headers.get('x-device-id', '').strip() else 'wechat'
    return 'unknown'


class ClientSourceRoute(APIRoute):
    def get_route_handler(self) -> Callable[[Request], Awaitable[Response]]:
        original_handler = super().get_route_handler()

        async def handle(request: Request) -> Response:
            token = current_client_source.set(resolve_client_source(request))
            try:
                return await original_handler(request)
            finally:
                current_client_source.reset(token)

        return handle