1 # SPDX-License-Identifier: GPL-2.0-only
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2023 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
14 from falcon.asgi import App, Request, Response
16 from nominatim.api import NominatimAPIAsync
17 import nominatim.api.v1 as api_impl
18 from nominatim.config import Configuration
20 class HTTPNominatimError(Exception):
21 """ A special exception class for errors raised during processing.
23 def __init__(self, msg: str, status: int, content_type: str) -> None:
26 self.content_type = content_type
29 async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
30 exception: HTTPNominatimError,
32 """ Special error handler that passes message and content type as
35 resp.status = exception.status
36 resp.text = exception.msg
37 resp.content_type = exception.content_type
40 async def timeout_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
41 exception: TimeoutError, #pylint: disable=unused-argument
43 """ Special error handler that passes message and content type as
47 resp.text = "Query took too long to process."
48 resp.content_type = 'text/plain; charset=utf-8'
51 class ParamWrapper(api_impl.ASGIAdaptor):
52 """ Adaptor class for server glue to Falcon framework.
55 def __init__(self, req: Request, resp: Response,
56 config: Configuration) -> None:
62 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
63 return cast(Optional[str], self.request.get_param(name, default=default))
66 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
67 return cast(Optional[str], self.request.get_header(name, default=default))
70 def error(self, msg: str, status: int = 400) -> HTTPNominatimError:
71 return HTTPNominatimError(msg, status, self.content_type)
74 def create_response(self, status: int, output: str, num_results: int) -> None:
75 self.response.context.num_results = num_results
76 self.response.status = status
77 self.response.text = output
78 self.response.content_type = self.content_type
81 def base_uri(self) -> str:
82 return cast (str, self.request.forwarded_prefix)
84 def config(self) -> Configuration:
88 class EndpointWrapper:
89 """ Converter for server glue endpoint functions to Falcon request handlers.
92 def __init__(self, name: str, func: api_impl.EndpointFunc, api: NominatimAPIAsync) -> None:
98 async def on_get(self, req: Request, resp: Response) -> None:
99 """ Implementation of the endpoint.
101 await self.func(self.api, ParamWrapper(req, resp, self.api.config))
104 class FileLoggingMiddleware:
105 """ Middleware to log selected requests into a file.
108 def __init__(self, file_name: str):
109 self.fd = open(file_name, 'a', buffering=1, encoding='utf8') # pylint: disable=R1732
112 async def process_request(self, req: Request, _: Response) -> None:
113 """ Callback before the request starts timing.
115 req.context.start = dt.datetime.now(tz=dt.timezone.utc)
118 async def process_response(self, req: Request, resp: Response,
119 resource: Optional[EndpointWrapper],
120 req_succeeded: bool) -> None:
121 """ Callback after requests writes to the logfile. It only
122 writes logs for sucessful requests for search, reverse and lookup.
124 if not req_succeeded or resource is None or resp.status != 200\
125 or resource.name not in ('reverse', 'search', 'lookup', 'details'):
128 finish = dt.datetime.now(tz=dt.timezone.utc)
129 duration = (finish - req.context.start).total_seconds()
130 params = req.scope['query_string'].decode('utf8')
131 start = req.context.start.replace(tzinfo=None)\
132 .isoformat(sep=' ', timespec='milliseconds')
134 self.fd.write(f"[{start}] "
135 f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
136 f'{resource.name} "{params}"\n')
140 """ Middleware that closes any open database connections.
143 def __init__(self, api: NominatimAPIAsync) -> None:
146 async def process_shutdown(self, *_: Any) -> None:
147 """Process the ASGI lifespan shutdown event.
149 await self.api.close()
152 def get_application(project_dir: Path,
153 environ: Optional[Mapping[str, str]] = None) -> App:
154 """ Create a Nominatim Falcon ASGI application.
156 api = NominatimAPIAsync(project_dir, environ)
158 middleware: List[object] = [APIShutdown(api)]
159 log_file = api.config.LOG_FILE
161 middleware.append(FileLoggingMiddleware(log_file))
163 app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'),
164 middleware=middleware)
165 app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
166 app.add_error_handler(TimeoutError, timeout_error_handler)
168 legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
169 for name, func in api_impl.ROUTES:
170 endpoint = EndpointWrapper(name, func, api)
171 app.add_route(f"/{name}", endpoint)
173 app.add_route(f"/{name}.php", endpoint)
178 def run_wsgi() -> App:
179 """ Entry point for uvicorn.
181 Make sure uvicorn is run from the project directory.
183 return get_application(Path('.'))