1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Server implementation using the falcon webserver framework.
10 from typing import Optional, Mapping, cast, Any, List
11 from pathlib import Path
15 from falcon.asgi import App, Request, Response
17 from nominatim_core.config import Configuration
18 from ...core import NominatimAPIAsync
19 from ... import v1 as api_impl
20 from ... import logging as loglib
22 class HTTPNominatimError(Exception):
23 """ A special exception class for errors raised during processing.
25 def __init__(self, msg: str, status: int, content_type: str) -> None:
28 self.content_type = content_type
31 async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
32 exception: HTTPNominatimError,
34 """ Special error handler that passes message and content type as
37 resp.status = exception.status
38 resp.text = exception.msg
39 resp.content_type = exception.content_type
42 async def timeout_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
43 exception: TimeoutError, #pylint: disable=unused-argument
45 """ Special error handler that passes message and content type as
50 loglib.log().comment('Aborted: Query took too long to process.')
51 logdata = loglib.get_and_disable()
54 resp.content_type = 'text/html; charset=utf-8'
56 resp.text = "Query took too long to process."
57 resp.content_type = 'text/plain; charset=utf-8'
60 class ParamWrapper(api_impl.ASGIAdaptor):
61 """ Adaptor class for server glue to Falcon framework.
64 def __init__(self, req: Request, resp: Response,
65 config: Configuration) -> None:
71 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
72 return cast(Optional[str], self.request.get_param(name, default=default))
75 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
76 return cast(Optional[str], self.request.get_header(name, default=default))
79 def error(self, msg: str, status: int = 400) -> HTTPNominatimError:
80 return HTTPNominatimError(msg, status, self.content_type)
83 def create_response(self, status: int, output: str, num_results: int) -> None:
84 self.response.context.num_results = num_results
85 self.response.status = status
86 self.response.text = output
87 self.response.content_type = self.content_type
90 def base_uri(self) -> str:
91 return cast (str, self.request.forwarded_prefix)
93 def config(self) -> Configuration:
97 class EndpointWrapper:
98 """ Converter for server glue endpoint functions to Falcon request handlers.
101 def __init__(self, name: str, func: api_impl.EndpointFunc, api: NominatimAPIAsync) -> None:
107 async def on_get(self, req: Request, resp: Response) -> None:
108 """ Implementation of the endpoint.
110 await self.func(self.api, ParamWrapper(req, resp, self.api.config))
113 class FileLoggingMiddleware:
114 """ Middleware to log selected requests into a file.
117 def __init__(self, file_name: str):
118 self.fd = open(file_name, 'a', buffering=1, encoding='utf8') # pylint: disable=R1732
121 async def process_request(self, req: Request, _: Response) -> None:
122 """ Callback before the request starts timing.
124 req.context.start = dt.datetime.now(tz=dt.timezone.utc)
127 async def process_response(self, req: Request, resp: Response,
128 resource: Optional[EndpointWrapper],
129 req_succeeded: bool) -> None:
130 """ Callback after requests writes to the logfile. It only
131 writes logs for successful requests for search, reverse and lookup.
133 if not req_succeeded or resource is None or resp.status != 200\
134 or resource.name not in ('reverse', 'search', 'lookup', 'details'):
137 finish = dt.datetime.now(tz=dt.timezone.utc)
138 duration = (finish - req.context.start).total_seconds()
139 params = req.scope['query_string'].decode('utf8')
140 start = req.context.start.replace(tzinfo=None)\
141 .isoformat(sep=' ', timespec='milliseconds')
143 self.fd.write(f"[{start}] "
144 f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
145 f'{resource.name} "{params}"\n')
149 """ Middleware that closes any open database connections.
152 def __init__(self, api: NominatimAPIAsync) -> None:
155 async def process_shutdown(self, *_: Any) -> None:
156 """Process the ASGI lifespan shutdown event.
158 await self.api.close()
161 def get_application(project_dir: Path,
162 environ: Optional[Mapping[str, str]] = None) -> App:
163 """ Create a Nominatim Falcon ASGI application.
165 api = NominatimAPIAsync(project_dir, environ)
167 middleware: List[object] = [APIShutdown(api)]
168 log_file = api.config.LOG_FILE
170 middleware.append(FileLoggingMiddleware(log_file))
172 app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'),
173 middleware=middleware)
174 app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
175 app.add_error_handler(TimeoutError, timeout_error_handler)
176 # different from TimeoutError in Python <= 3.10
177 app.add_error_handler(asyncio.TimeoutError, timeout_error_handler)
179 legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
180 for name, func in api_impl.ROUTES:
181 endpoint = EndpointWrapper(name, func, api)
182 app.add_route(f"/{name}", endpoint)
184 app.add_route(f"/{name}.php", endpoint)
189 def run_wsgi() -> App:
190 """ Entry point for uvicorn.
192 Make sure uvicorn is run from the project directory.
194 return get_application(Path('.'))