Skip to content

Commit 467b164

Browse files
committed
Make sure no exceptions escape our middleware stack
In Pyodide mode, an HTTPException(404) being thrown resulted in some default error handler (that happened not to be async) being run in a threadpool, which Pyodide could not handle. This change adds middleware in Pyodide mode only, which catches such errors and we return a plaintext response ourselves.
1 parent 0d49f16 commit 467b164

File tree

3 files changed

+45
-1
lines changed

3 files changed

+45
-1
lines changed

shiny/_app.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from htmltools import Tag, TagList, HTMLDocument, HTMLDependency, RenderedHTML
88

99
import starlette.applications
10+
import starlette.exceptions
1011
import starlette.middleware
1112
import starlette.routing
1213
import starlette.websockets
@@ -16,6 +17,8 @@
1617

1718
from ._autoreload import autoreload_url, InjectAutoreloadMiddleware
1819
from ._connection import Connection, StarletteConnection
20+
from ._shinyenv import is_pyodide
21+
from ._error import ErrorMiddleware
1922
from .html_dependencies import require_deps, jquery_deps, shiny_deps
2023
from .http_staticfiles import StaticFiles
2124
from .reactive import on_flushed
@@ -161,6 +164,8 @@ def init_starlette_app(self):
161164
middleware.append(
162165
starlette.middleware.Middleware(InjectAutoreloadMiddleware)
163166
)
167+
if is_pyodide:
168+
middleware.append(starlette.middleware.Middleware(ErrorMiddleware))
164169

165170
starlette_app = starlette.applications.Starlette(
166171
routes=routes,

shiny/_autoreload.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ async def rewrite_send(event: ASGISendEvent) -> None:
132132
event["body"] = body.replace(b"</head>", self.script, 1)
133133
body = b"" # Allow gc
134134
intercept = False
135-
elif event["more_body"]:
135+
elif "more_body" in event and event["more_body"]:
136136
# DO NOT send the response; wait for more data
137137
return
138138
else:

shiny/_error.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import cast
2+
3+
import starlette.exceptions as exceptions
4+
import starlette.responses as responses
5+
from starlette.types import Receive, Scope, Send, ASGIApp
6+
7+
8+
class ErrorMiddleware:
9+
"""Inserts shiny-autoreload.js into the head.
10+
11+
It's necessary to do it using middleware instead of in a nice htmldependency,
12+
because we want autoreload to be effective even when displaying an error page.
13+
"""
14+
15+
def __init__(self, app: ASGIApp):
16+
self.app = app
17+
18+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
19+
try:
20+
return await self.app(scope, receive, send)
21+
except exceptions.HTTPException as e:
22+
resp = responses.PlainTextResponse(
23+
e.detail,
24+
e.status_code,
25+
headers=cast(
26+
dict[str, str],
27+
e.headers, # pyright: ignore[reportUnknownMemberType]
28+
),
29+
media_type="text/plain",
30+
)
31+
await resp(scope, receive, send)
32+
except Exception as e:
33+
# Seems super weird this is just going to stdout, should we use logger or
34+
# at least stderr?
35+
print("Unhandled error: " + str(e))
36+
resp = responses.PlainTextResponse(
37+
"An internal server error occurred", 500, media_type="text/plain"
38+
)
39+
await resp(scope, receive, send)

0 commit comments

Comments
 (0)