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 ...config import Configuration
18 from ...core import NominatimAPIAsync
19 from ... import v1 as api_impl
20 from ... import logging as loglib
21 from ..asgi_adaptor import ASGIAdaptor, EndpointFunc
23 class HTTPNominatimError(Exception):
24 """ A special exception class for errors raised during processing.
26 def __init__(self, msg: str, status: int, content_type: str) -> None:
29 self.content_type = content_type
32 async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
33 exception: HTTPNominatimError,
35 """ Special error handler that passes message and content type as
38 resp.status = exception.status
39 resp.text = exception.msg
40 resp.content_type = exception.content_type
43 async def timeout_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
44 exception: TimeoutError, #pylint: disable=unused-argument
46 """ Special error handler that passes message and content type as
51 loglib.log().comment('Aborted: Query took too long to process.')
52 logdata = loglib.get_and_disable()
55 resp.content_type = 'text/html; charset=utf-8'
57 resp.text = "Query took too long to process."
58 resp.content_type = 'text/plain; charset=utf-8'
61 class ParamWrapper(ASGIAdaptor):
62 """ Adaptor class for server glue to Falcon framework.
65 def __init__(self, req: Request, resp: Response,
66 config: Configuration) -> None:
72 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
73 return cast(Optional[str], self.request.get_param(name, default=default))
76 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
77 return cast(Optional[str], self.request.get_header(name, default=default))
80 def error(self, msg: str, status: int = 400) -> HTTPNominatimError:
81 return HTTPNominatimError(msg, status, self.content_type)
84 def create_response(self, status: int, output: str, num_results: int) -> None:
85 self.response.context.num_results = num_results
86 self.response.status = status
87 self.response.text = output
88 self.response.content_type = self.content_type
91 def base_uri(self) -> str:
92 return cast (str, self.request.forwarded_prefix)
94 def config(self) -> Configuration:
98 class EndpointWrapper:
99 """ Converter for server glue endpoint functions to Falcon request handlers.
102 def __init__(self, name: str, func: EndpointFunc, api: NominatimAPIAsync) -> None:
108 async def on_get(self, req: Request, resp: Response) -> None:
109 """ Implementation of the endpoint.
111 await self.func(self.api, ParamWrapper(req, resp, self.api.config))
114 class FileLoggingMiddleware:
115 """ Middleware to log selected requests into a file.
118 def __init__(self, file_name: str):
119 self.fd = open(file_name, 'a', buffering=1, encoding='utf8') # pylint: disable=R1732
122 async def process_request(self, req: Request, _: Response) -> None:
123 """ Callback before the request starts timing.
125 req.context.start = dt.datetime.now(tz=dt.timezone.utc)
128 async def process_response(self, req: Request, resp: Response,
129 resource: Optional[EndpointWrapper],
130 req_succeeded: bool) -> None:
131 """ Callback after requests writes to the logfile. It only
132 writes logs for successful requests for search, reverse and lookup.
134 if not req_succeeded or resource is None or resp.status != 200\
135 or resource.name not in ('reverse', 'search', 'lookup', 'details'):
138 finish = dt.datetime.now(tz=dt.timezone.utc)
139 duration = (finish - req.context.start).total_seconds()
140 params = req.scope['query_string'].decode('utf8')
141 start = req.context.start.replace(tzinfo=None)\
142 .isoformat(sep=' ', timespec='milliseconds')
144 self.fd.write(f"[{start}] "
145 f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
146 f'{resource.name} "{params}"\n')
150 """ Middleware that closes any open database connections.
153 def __init__(self, api: NominatimAPIAsync) -> None:
156 async def process_shutdown(self, *_: Any) -> None:
157 """Process the ASGI lifespan shutdown event.
159 await self.api.close()
162 def get_application(project_dir: Path,
163 environ: Optional[Mapping[str, str]] = None) -> App:
164 """ Create a Nominatim Falcon ASGI application.
166 api = NominatimAPIAsync(project_dir, environ)
168 middleware: List[object] = [APIShutdown(api)]
169 log_file = api.config.LOG_FILE
171 middleware.append(FileLoggingMiddleware(log_file))
173 app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'),
174 middleware=middleware)
175 app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
176 app.add_error_handler(TimeoutError, timeout_error_handler)
177 # different from TimeoutError in Python <= 3.10
178 app.add_error_handler(asyncio.TimeoutError, timeout_error_handler)
180 legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
181 for name, func in api_impl.ROUTES:
182 endpoint = EndpointWrapper(name, func, api)
183 app.add_route(f"/{name}", endpoint)
185 app.add_route(f"/{name}.php", endpoint)
190 def run_wsgi() -> App:
191 """ Entry point for uvicorn.
193 Make sure uvicorn is run from the project directory.
195 return get_application(Path('.'))