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, 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, load_format_dispatcher
21 from ... import logging as loglib
22 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,
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,
46 exception: TimeoutError,
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,
68 config: Configuration, formatter: FormatDispatcher) -> None:
72 self._formatter = formatter
74 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
75 return self.request.get_param(name, default=default)
77 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
78 return 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)
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
89 def base_uri(self) -> str:
90 return self.request.forwarded_prefix
92 def config(self) -> Configuration:
95 def formatting(self) -> FormatDispatcher:
96 return self._formatter
99 class EndpointWrapper:
100 """ Converter for server glue endpoint functions to Falcon request handlers.
103 def __init__(self, name: str, func: EndpointFunc, api: NominatimAPIAsync,
104 formatter: FormatDispatcher) -> None:
108 self.formatter = formatter
110 async def on_get(self, req: Request, resp: Response) -> None:
111 """ Implementation of the endpoint.
113 await self.func(self.api, ParamWrapper(req, resp, self.api.config,
117 class FileLoggingMiddleware:
118 """ Middleware to log selected requests into a file.
121 def __init__(self, file_name: str):
122 self.fd = open(file_name, 'a', buffering=1, encoding='utf8')
124 async def process_request(self, req: Request, _: Response) -> None:
125 """ Callback before the request starts timing.
127 req.context.start = dt.datetime.now(tz=dt.timezone.utc)
129 async def process_response(self, req: Request, resp: Response,
130 resource: Optional[EndpointWrapper],
131 req_succeeded: bool) -> None:
132 """ Callback after requests writes to the logfile. It only
133 writes logs for successful requests for search, reverse and lookup.
135 if not req_succeeded or resource is None or resp.status != 200\
136 or resource.name not in ('reverse', 'search', 'lookup', 'details'):
139 finish = dt.datetime.now(tz=dt.timezone.utc)
140 duration = (finish - req.context.start).total_seconds()
141 params = req.scope['query_string'].decode('utf8')
142 start = req.context.start.replace(tzinfo=None)\
143 .isoformat(sep=' ', timespec='milliseconds')
145 self.fd.write(f"[{start}] "
146 f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
147 f'{resource.name} "{params}"\n')
151 """ Middleware managing the Nominatim database connection.
154 def __init__(self, project_dir: Path, environ: Optional[Mapping[str, str]]) -> None:
155 self.api = NominatimAPIAsync(project_dir, environ)
156 self.app: Optional[App] = None
159 def config(self) -> Configuration:
160 """ Get the configuration for Nominatim.
162 return self.api.config
164 def set_app(self, app: App) -> None:
165 """ Set the Falcon application this middleware is connected to.
169 async def process_startup(self, *_: Any) -> None:
170 """ Process the ASGI lifespan startup event.
172 assert self.app is not None
173 legacy_urls = self.api.config.get_bool('SERVE_LEGACY_URLS')
174 formatter = load_format_dispatcher('v1', self.api.config.project_dir)
175 for name, func in await api_impl.get_routes(self.api):
176 endpoint = EndpointWrapper(name, func, self.api, formatter)
177 self.app.add_route(f"/{name}", endpoint)
179 self.app.add_route(f"/{name}.php", endpoint)
181 async def process_shutdown(self, *_: Any) -> None:
182 """Process the ASGI lifespan shutdown event.
184 await self.api.close()
187 def get_application(project_dir: Path,
188 environ: Optional[Mapping[str, str]] = None) -> App:
189 """ Create a Nominatim Falcon ASGI application.
191 apimw = APIMiddleware(project_dir, environ)
193 middleware: List[object] = [apimw]
194 log_file = apimw.config.LOG_FILE
196 middleware.append(FileLoggingMiddleware(log_file))
198 app = App(cors_enable=apimw.config.get_bool('CORS_NOACCESSCONTROL'),
199 middleware=middleware)
202 app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
203 app.add_error_handler(TimeoutError, timeout_error_handler)
204 # different from TimeoutError in Python <= 3.10
205 app.add_error_handler(asyncio.TimeoutError, timeout_error_handler) # type: ignore[arg-type]
210 def run_wsgi() -> App:
211 """ Entry point for uvicorn.
213 Make sure uvicorn is run from the project directory.
215 return get_application(Path('.'))