1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2025 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, Any, List, cast
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 ...types import QueryStatistics
20 from ... import v1 as api_impl
21 from ...result_formatting import FormatDispatcher, load_format_dispatcher
22 from ... import logging as loglib
23 from ..asgi_adaptor import ASGIAdaptor, EndpointFunc
26 class HTTPNominatimError(Exception):
27 """ A special exception class for errors raised during processing.
29 def __init__(self, msg: str, status: int, content_type: str) -> None:
32 self.content_type = content_type
35 async def nominatim_error_handler(req: Request, resp: Response,
36 exception: HTTPNominatimError,
38 """ Special error handler that passes message and content type as
41 resp.status = exception.status
42 resp.text = exception.msg
43 resp.content_type = exception.content_type
46 async def timeout_error_handler(req: Request, resp: Response,
47 exception: TimeoutError,
49 """ Special error handler that passes message and content type as
54 loglib.log().comment('Aborted: Query took too long to process.')
55 logdata = loglib.get_and_disable()
58 resp.content_type = 'text/html; charset=utf-8'
60 resp.text = "Query took too long to process."
61 resp.content_type = 'text/plain; charset=utf-8'
64 class ParamWrapper(ASGIAdaptor):
65 """ Adaptor class for server glue to Falcon framework.
68 def __init__(self, req: Request, resp: Response,
69 config: Configuration, formatter: FormatDispatcher) -> None:
73 self._formatter = formatter
75 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
76 return self.request.get_param(name, default=default)
78 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
79 return 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)
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
90 def base_uri(self) -> str:
91 return self.request.forwarded_prefix
93 def config(self) -> Configuration:
96 def formatting(self) -> FormatDispatcher:
97 return self._formatter
99 def query_stats(self) -> Optional[QueryStatistics]:
100 return cast(Optional[QueryStatistics], getattr(self.request.context, 'query_stats', None))
103 class EndpointWrapper:
104 """ Converter for server glue endpoint functions to Falcon request handlers.
107 def __init__(self, name: str, func: EndpointFunc, api: NominatimAPIAsync,
108 formatter: FormatDispatcher) -> None:
112 self.formatter = formatter
114 async def on_get(self, req: Request, resp: Response) -> None:
115 """ Implementation of the endpoint.
117 await self.func(self.api, ParamWrapper(req, resp, self.api.config,
121 class FileLoggingMiddleware:
122 """ Middleware to log selected requests into a file.
125 def __init__(self, file_name: str):
126 self.fd = open(file_name, 'a', buffering=1, encoding='utf8')
128 async def process_request(self, req: Request, _: Response) -> None:
129 """ Callback before the request starts timing.
131 req.context.query_stats = QueryStatistics()
133 async def process_response(self, req: Request, resp: Response,
134 resource: Optional[EndpointWrapper],
135 req_succeeded: bool) -> None:
136 """ Callback after requests writes to the logfile. It only
137 writes logs for successful requests for search, reverse and lookup.
139 qs = req.context.query_stats
141 if not req_succeeded or 'start' not in qs\
142 or resource is None or resp.status != 200\
143 or resource.name not in ('reverse', 'search', 'lookup', 'details'):
146 qs['endpoint'] = resource.name
147 qs['query_string'] = req.scope['query_string'].decode('utf8')
148 qs['results_total'] = getattr(resp.context, 'num_results', 0)
149 for param in ('start', 'end', 'start_query'):
150 if isinstance(qs.get(param), dt.datetime):
151 qs[param] = qs[param].replace(tzinfo=None)\
152 .isoformat(sep=' ', timespec='milliseconds')
154 self.fd.write(("[{start}] {total_time:.4f} {results_total} "
155 '{endpoint} "{query_string}"\n').format_map(qs))
159 """ Middleware managing the Nominatim database connection.
162 def __init__(self, project_dir: Path, environ: Optional[Mapping[str, str]]) -> None:
163 self.api = NominatimAPIAsync(project_dir, environ)
164 self.app: Optional[App] = None
167 def config(self) -> Configuration:
168 """ Get the configuration for Nominatim.
170 return self.api.config
172 def set_app(self, app: App) -> None:
173 """ Set the Falcon application this middleware is connected to.
177 async def process_startup(self, *_: Any) -> None:
178 """ Process the ASGI lifespan startup event.
180 assert self.app is not None
181 legacy_urls = self.api.config.get_bool('SERVE_LEGACY_URLS')
182 formatter = load_format_dispatcher('v1', self.api.config.project_dir)
183 for name, func in await api_impl.get_routes(self.api):
184 endpoint = EndpointWrapper(name, func, self.api, formatter)
185 self.app.add_route(f"/{name}", endpoint)
187 self.app.add_route(f"/{name}.php", endpoint)
189 async def process_shutdown(self, *_: Any) -> None:
190 """Process the ASGI lifespan shutdown event.
192 await self.api.close()
195 def get_application(project_dir: Path,
196 environ: Optional[Mapping[str, str]] = None) -> App:
197 """ Create a Nominatim Falcon ASGI application.
199 apimw = APIMiddleware(project_dir, environ)
201 middleware: List[Any] = [apimw]
202 log_file = apimw.config.LOG_FILE
204 middleware.append(FileLoggingMiddleware(log_file))
206 app = App(cors_enable=apimw.config.get_bool('CORS_NOACCESSCONTROL'),
207 middleware=middleware)
210 app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
211 app.add_error_handler(TimeoutError, timeout_error_handler)
212 # different from TimeoutError in Python <= 3.10
213 app.add_error_handler(asyncio.TimeoutError, timeout_error_handler) # type: ignore[arg-type]
218 def run_wsgi() -> App:
219 """ Entry point for uvicorn.
221 Make sure uvicorn is run from the project directory.
223 return get_application(Path('.'))