Skip to content

Commit c786eeb

Browse files
authored
Merge pull request #260 from rstudio/fix-error-thread
Make sure no exceptions escape our middleware stack
2 parents 9efcda8 + 95b3021 commit c786eeb

File tree

4 files changed

+53
-1
lines changed

4 files changed

+53
-1
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ typings/uvicorn/__init__.pyi:
5454
check: typings/uvicorn/__init__.pyi ## type check with pyright
5555
pyright
5656

57+
check-old: typings/uvicorn/__init__.pyi ## type check with pyright
58+
pyright --pythonversion=3.7
59+
5760
lint: ## check style with flake8
5861
flake8 --show-source --max-line-length=127 shiny tests examples
5962

shiny/_app.py

Lines changed: 10 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,13 @@ def init_starlette_app(self):
161164
middleware.append(
162165
starlette.middleware.Middleware(InjectAutoreloadMiddleware)
163166
)
167+
# In Pyodide mode, an HTTPException(404) being thrown resulted in
168+
# some default error handler (that happened not to be async) being
169+
# run in a threadpool, which Pyodide could not handle. So in Pyodide
170+
# mode, install our own async error handler at the outermost layer
171+
# that we can.
172+
if is_pyodide:
173+
middleware.append(starlette.middleware.Middleware(ErrorMiddleware))
164174

165175
starlette_app = starlette.applications.Starlette(
166176
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 Dict, cast
2+
3+
import starlette.exceptions as exceptions
4+
import starlette.responses as responses
5+
from starlette.types import ASGIApp, Receive, Scope, Send
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)