1111import json
1212from dataclasses import dataclass
1313from pathlib import Path
14+ import logging
1415from traceback import format_exception
1516from typing import Any , TYPE_CHECKING
1617
2425if TYPE_CHECKING :
2526 from . import ThingServer
2627
28+ LOGGER = logging .getLogger (__name__ )
29+
2730_TEMPLATE_PATH = Path (__file__ ).with_name ("fallback.html.jinja" )
2831
2932
@@ -46,6 +49,19 @@ class FallbackContext:
4649 """Any logging history to show."""
4750
4851
52+ LAST_RESORT_PAGE = """
53+ <html>
54+ <head lang="en">
55+ <title>LabThings Internal Error</title>
56+ </head>
57+ <body>
58+ <h1>LabThings Internal Error</h1>
59+ <p>Couldn't start LabThings Server.</p>
60+ <p>A further error occurred when gathering context.</p>
61+ </body>
62+ """
63+
64+
4965class FallbackApp (FastAPI ):
5066 """A basic FastAPI application to serve a LabThings error page."""
5167
@@ -59,13 +75,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
5975 :param \**kwargs: is passed to `fastapi.FastAPI.__init__`\ .
6076 """
6177 super ().__init__ (* args , ** kwargs )
62- # Handle dictionary config here for legacy reasons.
63- self ._context : FallbackContext | None = None
64- self ._env = Environment (
65- loader = BaseLoader (),
66- autoescape = select_autoescape (["html" , "xml" ]),
67- )
68- self .set_template_str (_TEMPLATE_PATH .read_text (encoding = "utf-8" ))
78+ try :
79+ # Handle dictionary config here for legacy reasons.
80+ self ._context : FallbackContext | None = None
81+ self ._env = Environment (
82+ loader = BaseLoader (),
83+ autoescape = select_autoescape (["html" , "xml" ]),
84+ )
85+ self .set_template_str (_TEMPLATE_PATH .read_text (encoding = "utf-8" ))
86+ except BaseException as e :
87+ # Catch any error and continue or there is no fallback server
88+ LOGGER .exception (e )
6989 self .html_code = 500
7090
7191 def set_context (self , context : FallbackContext ) -> None :
@@ -101,26 +121,30 @@ def fallback_page(self) -> HTMLResponse:
101121 :return: The HTMLResponse for the fallback page.
102122 :raises RuntimeError: if the fallback context was never set.
103123 """
104- if self ._context is None :
105- raise RuntimeError ("Not context set for fallback server." )
106-
107- error_message , error_w_trace = _format_error_and_traceback (self ._context )
108- things = list (self ._context .server .things ) if self ._context .server else []
109-
110- if isinstance (self ._context .config , ThingServerConfig ):
111- conf_str = self ._context .config .model_dump_json (indent = 2 )
112- else :
113- conf_str = json .dumps (self ._context .config , indent = 2 )
114-
115- content = app ._template .render (
116- error_message = error_message ,
117- things = things ,
118- config = conf_str ,
119- traceback = error_w_trace ,
120- logginginfo = self ._context .log_history ,
121- )
122-
123- return HTMLResponse (content = content , status_code = app .html_code )
124+ try :
125+ if self ._context is None :
126+ raise RuntimeError ("Not context set for fallback server." )
127+ error_message , error_w_trace = _format_error_and_traceback (self ._context )
128+ things = list (self ._context .server .things ) if self ._context .server else []
129+
130+ if isinstance (self ._context .config , ThingServerConfig ):
131+ conf_str = self ._context .config .model_dump_json (indent = 2 )
132+ else :
133+ conf_str = json .dumps (self ._context .config , indent = 2 )
134+
135+ content = app ._template .render (
136+ error_message = error_message ,
137+ things = things ,
138+ config = conf_str ,
139+ traceback = error_w_trace ,
140+ logginginfo = self ._context .log_history ,
141+ )
142+
143+ return HTMLResponse (content = content , status_code = app .html_code )
144+ except BaseException as e :
145+ # Catch any error and continue or there is no fallback server
146+ LOGGER .exception (e )
147+ return HTMLResponse (content = LAST_RESORT_PAGE , status_code = 500 )
124148
125149
126150app = FallbackApp ()
0 commit comments