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 ...result_formatting import FormatDispatcher
21 from ...v1.format import dispatch as formatting
22 from ... import logging as loglib
23 from ..asgi_adaptor import ASGIAdaptor, EndpointFunc
25 class HTTPNominatimError(Exception):
26 """ A special exception class for errors raised during processing.
28 def __init__(self, msg: str, status: int, content_type: str) -> None:
31 self.content_type = content_type
34 async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
35 exception: HTTPNominatimError,
37 """ Special error handler that passes message and content type as
40 resp.status = exception.status
41 resp.text = exception.msg
42 resp.content_type = exception.content_type
45 async def timeout_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
46 exception: TimeoutError, #pylint: disable=unused-argument
48 """ Special error handler that passes message and content type as
53 loglib.log().comment('Aborted: Query took too long to process.')
54 logdata = loglib.get_and_disable()
57 resp.content_type = 'text/html; charset=utf-8'
59 resp.text = "Query took too long to process."
60 resp.content_type = 'text/plain; charset=utf-8'
63 class ParamWrapper(ASGIAdaptor):
64 """ Adaptor class for server glue to Falcon framework.
67 def __init__(self, req: Request, resp: Response, config: Configuration) -> None:
73 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
74 return cast(Optional[str], self.request.get_param(name, default=default))
77 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
78 return cast(Optional[str], self.request.get_header(name, default=default))
81 def error(self, msg: str, status: int = 400) -> HTTPNominatimError:
82 return HTTPNominatimError(msg, status, self.content_type)
85 def create_response(self, status: int, output: str, num_results: int) -> None:
86 self.response.context.num_results = num_results
87 self.response.status = status
88 self.response.text = output
89 self.response.content_type = self.content_type
92 def base_uri(self) -> str:
93 return cast (str, self.request.forwarded_prefix)
95 def config(self) -> Configuration:
98 def formatting(self) -> FormatDispatcher:
102 class EndpointWrapper:
103 """ Converter for server glue endpoint functions to Falcon request handlers.
106 def __init__(self, name: str, func: EndpointFunc, api: NominatimAPIAsync) -> None:
112 async def on_get(self, req: Request, resp: Response) -> None:
113 """ Implementation of the endpoint.
115 await self.func(self.api, ParamWrapper(req, resp, self.api.config))
118 class FileLoggingMiddleware:
119 """ Middleware to log selected requests into a file.
122 def __init__(self, file_name: str):
123 self.fd = open(file_name, 'a', buffering=1, encoding='utf8') # pylint: disable=R1732
126 async def process_request(self, req: Request, _: Response) -> None:
127 """ Callback before the request starts timing.
129 req.context.start = dt.datetime.now(tz=dt.timezone.utc)
132 async def process_response(self, req: Request, resp: Response,
133 resource: Optional[EndpointWrapper],
134 req_succeeded: bool) -> None:
135 """ Callback after requests writes to the logfile. It only
136 writes logs for successful requests for search, reverse and lookup.
138 if not req_succeeded or resource is None or resp.status != 200\
139 or resource.name not in ('reverse', 'search', 'lookup', 'details'):
142 finish = dt.datetime.now(tz=dt.timezone.utc)
143 duration = (finish - req.context.start).total_seconds()
144 params = req.scope['query_string'].decode('utf8')
145 start = req.context.start.replace(tzinfo=None)\
146 .isoformat(sep=' ', timespec='milliseconds')
148 self.fd.write(f"[{start}] "
149 f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
150 f'{resource.name} "{params}"\n')
154 """ Middleware that closes any open database connections.
157 def __init__(self, api: NominatimAPIAsync) -> None:
160 async def process_shutdown(self, *_: Any) -> None:
161 """Process the ASGI lifespan shutdown event.
163 await self.api.close()
166 def get_application(project_dir: Path,
167 environ: Optional[Mapping[str, str]] = None) -> App:
168 """ Create a Nominatim Falcon ASGI application.
170 api = NominatimAPIAsync(project_dir, environ)
172 middleware: List[object] = [APIShutdown(api)]
173 log_file = api.config.LOG_FILE
175 middleware.append(FileLoggingMiddleware(log_file))
177 app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'),
178 middleware=middleware)
179 app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
180 app.add_error_handler(TimeoutError, timeout_error_handler)
181 # different from TimeoutError in Python <= 3.10
182 app.add_error_handler(asyncio.TimeoutError, timeout_error_handler)
184 legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
185 for name, func in api_impl.ROUTES:
186 endpoint = EndpointWrapper(name, func, api)
187 app.add_route(f"/{name}", endpoint)
189 app.add_route(f"/{name}.php", endpoint)
194 def run_wsgi() -> App:
195 """ Entry point for uvicorn.
197 Make sure uvicorn is run from the project directory.
199 return get_application(Path('.'))