@@ -59,6 +59,7 @@ def reload_begin():
5959# Called from child process when new application instance starts up
6060def reload_end ():
6161 import websockets
62+ import websockets .asyncio .client
6263
6364 # os.kill(os.getppid(), signal.SIGUSR1)
6465
@@ -70,12 +71,12 @@ def reload_end():
7071
7172 async def _ () -> None :
7273 options = {
73- "extra_headers " : {
74+ "additional_headers " : {
7475 "Shiny-Autoreload-Secret" : os .getenv ("SHINY_AUTORELOAD_SECRET" , "" ),
7576 }
7677 }
7778 try :
78- async with websockets .connect (
79+ async with websockets .asyncio . client . connect (
7980 url , ** options # pyright: ignore[reportArgumentType]
8081 ) as websocket :
8182 await websocket .send ("reload_end" )
@@ -169,6 +170,17 @@ def start_server(port: int, app_port: int, launch_browser: bool):
169170 os .environ ["SHINY_AUTORELOAD_PORT" ] = str (port )
170171 os .environ ["SHINY_AUTORELOAD_SECRET" ] = secret
171172
173+ # websockets 14.0 (and presumably later) log an error if a connection is opened and
174+ # closed before any data is sent. Our VS Code extension does exactly this--opens a
175+ # connection to check if the server is running, then closes it. It's better that it
176+ # does this and doesn't actually perform an HTTP request because we can't guarantee
177+ # that the HTTP request will be cheap (we do the same ping on both the autoreload
178+ # socket and the main uvicorn socket). So better to just suppress all errors until
179+ # we think we have a problem. You can unsuppress by setting the environment variable
180+ # to DEBUG.
181+ loglevel = os .getenv ("SHINY_AUTORELOAD_LOG_LEVEL" , "CRITICAL" )
182+ logging .getLogger ("websockets" ).setLevel (loglevel )
183+
172184 app_url = get_proxy_url (f"http://127.0.0.1:{ app_port } /" )
173185
174186 # Run on a background thread so our event loop doesn't interfere with uvicorn.
@@ -186,6 +198,8 @@ async def _coro_main(
186198 port : int , app_url : str , secret : str , launch_browser : bool
187199) -> None :
188200 import websockets
201+ import websockets .asyncio .server
202+ from websockets .http11 import Request , Response
189203
190204 reload_now : asyncio .Event = asyncio .Event ()
191205
@@ -198,18 +212,22 @@ def nudge():
198212 reload_now .set ()
199213 reload_now .clear ()
200214
201- async def reload_server (conn : websockets .server .WebSocketServerProtocol ):
215+ async def reload_server (conn : websockets .asyncio . server .ServerConnection ):
202216 try :
203- if conn .path == "/autoreload" :
217+ if conn .request is None :
218+ raise RuntimeError (
219+ "Autoreload server received a connection with no request"
220+ )
221+ elif conn .request .path == "/autoreload" :
204222 # The client wants to be notified when the app has reloaded. The client
205223 # in this case is the web browser, specifically shiny-autoreload.js.
206224 while True :
207225 await reload_now .wait ()
208226 await conn .send ("autoreload" )
209- elif conn .path == "/notify" :
227+ elif conn .request . path == "/notify" :
210228 # The client is notifying us that the app has reloaded. The client in
211229 # this case is the uvicorn worker process (see reload_end(), above).
212- req_secret = conn .request_headers .get ("Shiny-Autoreload-Secret" , "" )
230+ req_secret = conn .request . headers .get ("Shiny-Autoreload-Secret" , "" )
213231 if req_secret != secret :
214232 # The client couldn't prove that they were from a child process
215233 return
@@ -225,18 +243,20 @@ async def reload_server(conn: websockets.server.WebSocketServerProtocol):
225243 # VSCode extension used in RSW sniffs out ports that are being listened on, which
226244 # leads to confusion if all you get is an error.
227245 async def process_request (
228- path : str , request_headers : websockets .datastructures .Headers
229- ) -> Optional [tuple [http .HTTPStatus , websockets .datastructures .HeadersLike , bytes ]]:
230- # If there's no Upgrade header, it's not a WebSocket request.
231- if request_headers .get ("Upgrade" ) is None :
232- # For some unknown reason, this fixes a tendency on GitHub Codespaces to
233- # correctly proxy through this request, but give a 404 when the redirect is
234- # followed and app_url is requested. With the sleep, both requests tend to
235- # succeed reliably.
236- await asyncio .sleep (1 )
237- return (http .HTTPStatus .MOVED_PERMANENTLY , [("Location" , app_url )], b"" )
238-
239- async with websockets .serve (
246+ connection : websockets .asyncio .server .ServerConnection ,
247+ request : Request ,
248+ ) -> Response | None :
249+ if request .headers .get ("Upgrade" ) is None :
250+ return Response (
251+ status_code = http .HTTPStatus .MOVED_PERMANENTLY ,
252+ reason_phrase = "Moved Permanently" ,
253+ headers = websockets .Headers (Location = app_url ),
254+ body = None ,
255+ )
256+ else :
257+ return None
258+
259+ async with websockets .asyncio .server .serve (
240260 reload_server , "127.0.0.1" , port , process_request = process_request
241261 ):
242262 await asyncio .Future () # wait forever
0 commit comments