@vercel/python 5.0.5 → 5.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +807 -232
- package/package.json +3 -2
- package/vc_init.py +228 -107
- package/vc_init_dev_asgi.py +58 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/python",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.7",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
|
-
"vc_init.py"
|
|
9
|
+
"vc_init.py",
|
|
10
|
+
"vc_init_dev_asgi.py"
|
|
10
11
|
],
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
package/vc_init.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import sys
|
|
2
3
|
import os
|
|
3
4
|
import site
|
|
@@ -5,9 +6,19 @@ import importlib
|
|
|
5
6
|
import base64
|
|
6
7
|
import json
|
|
7
8
|
import inspect
|
|
9
|
+
import threading
|
|
10
|
+
import asyncio
|
|
11
|
+
import http
|
|
12
|
+
import time
|
|
8
13
|
from importlib import util
|
|
9
|
-
from http.server import BaseHTTPRequestHandler
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
15
|
import socket
|
|
16
|
+
import functools
|
|
17
|
+
import logging
|
|
18
|
+
import builtins
|
|
19
|
+
from typing import Callable, Literal
|
|
20
|
+
import contextvars
|
|
21
|
+
import io
|
|
11
22
|
|
|
12
23
|
_here = os.path.dirname(__file__)
|
|
13
24
|
_vendor_rel = '__VC_HANDLER_VENDOR_DIR'
|
|
@@ -51,72 +62,69 @@ def format_headers(headers, decode=False):
|
|
|
51
62
|
keyToList[key].append(value)
|
|
52
63
|
return keyToList
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
import builtins
|
|
61
|
-
import logging
|
|
65
|
+
# Custom logging handler so logs are properly categorized
|
|
66
|
+
class VCLogHandler(logging.Handler):
|
|
67
|
+
def __init__(self, send_message: Callable[[dict], None], context_getter: Callable[[], dict] | None = None):
|
|
68
|
+
super().__init__()
|
|
69
|
+
self._send_message = send_message
|
|
70
|
+
self._context_getter = context_getter
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
def emit(self, record):
|
|
73
|
+
try:
|
|
74
|
+
message = record.getMessage()
|
|
75
|
+
except Exception:
|
|
76
|
+
try:
|
|
77
|
+
message = f"{record.msg}"
|
|
78
|
+
except Exception:
|
|
79
|
+
message = ""
|
|
80
|
+
|
|
81
|
+
if record.levelno >= logging.CRITICAL:
|
|
82
|
+
level = "fatal"
|
|
83
|
+
elif record.levelno >= logging.ERROR:
|
|
84
|
+
level = "error"
|
|
85
|
+
elif record.levelno >= logging.WARNING:
|
|
86
|
+
level = "warn"
|
|
87
|
+
elif record.levelno >= logging.INFO:
|
|
88
|
+
level = "info"
|
|
89
|
+
else:
|
|
90
|
+
level = "debug"
|
|
66
91
|
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
ctx = None
|
|
93
|
+
try:
|
|
94
|
+
ctx = self._context_getter() if self._context_getter is not None else None
|
|
95
|
+
except Exception:
|
|
96
|
+
ctx = None
|
|
69
97
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
if ctx is not None:
|
|
99
|
+
try:
|
|
100
|
+
self._send_message({
|
|
101
|
+
"type": "log",
|
|
102
|
+
"payload": {
|
|
103
|
+
"context": {
|
|
104
|
+
"invocationId": ctx['invocationId'],
|
|
105
|
+
"requestId": ctx['requestId'],
|
|
106
|
+
},
|
|
107
|
+
"message": base64.b64encode(message.encode()).decode(),
|
|
108
|
+
"level": level,
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
else:
|
|
114
|
+
try:
|
|
115
|
+
sys.stdout.write(message + "\n")
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
74
118
|
|
|
75
|
-
def timed_request(func):
|
|
76
|
-
fetchId = 0
|
|
77
|
-
@functools.wraps(func)
|
|
78
|
-
def wrapper(self, method, url, *args, **kwargs):
|
|
79
|
-
nonlocal fetchId
|
|
80
|
-
fetchId += 1
|
|
81
|
-
start_time = int(time.time() * 1000)
|
|
82
|
-
result = func(self, method, url, *args, **kwargs)
|
|
83
|
-
elapsed_time = int(time.time() * 1000) - start_time
|
|
84
|
-
parsed_url = urlparse(url)
|
|
85
|
-
context = storage.get()
|
|
86
|
-
if context is not None:
|
|
87
|
-
send_message({
|
|
88
|
-
"type": "metric",
|
|
89
|
-
"payload": {
|
|
90
|
-
"context": {
|
|
91
|
-
"invocationId": context['invocationId'],
|
|
92
|
-
"requestId": context['requestId'],
|
|
93
|
-
},
|
|
94
|
-
"type": "fetch-metric",
|
|
95
|
-
"payload": {
|
|
96
|
-
"pathname": parsed_url.path,
|
|
97
|
-
"search": parsed_url.query,
|
|
98
|
-
"start": start_time,
|
|
99
|
-
"duration": elapsed_time,
|
|
100
|
-
"host": parsed_url.hostname or self.host,
|
|
101
|
-
"statusCode": result.status,
|
|
102
|
-
"method": method,
|
|
103
|
-
"id": fetchId
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
return result
|
|
108
|
-
return wrapper
|
|
109
|
-
urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
|
|
110
|
-
except:
|
|
111
|
-
pass
|
|
112
119
|
|
|
120
|
+
def setup_logging(send_message: Callable[[dict], None], storage: contextvars.ContextVar[dict | None]):
|
|
113
121
|
# Override sys.stdout and sys.stderr to map logs to the correct request
|
|
114
122
|
class StreamWrapper:
|
|
115
|
-
def __init__(self, stream, stream_name):
|
|
123
|
+
def __init__(self, stream: io.TextIOBase, stream_name: Literal["stdout", "stderr"]):
|
|
116
124
|
self.stream = stream
|
|
117
125
|
self.stream_name = stream_name
|
|
118
126
|
|
|
119
|
-
def write(self, message):
|
|
127
|
+
def write(self, message: str):
|
|
120
128
|
context = storage.get()
|
|
121
129
|
if context is not None:
|
|
122
130
|
send_message({
|
|
@@ -139,19 +147,15 @@ if 'VERCEL_IPC_PATH' in os.environ:
|
|
|
139
147
|
sys.stdout = StreamWrapper(sys.stdout, "stdout")
|
|
140
148
|
sys.stderr = StreamWrapper(sys.stderr, "stderr")
|
|
141
149
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def wrapper(*args, **kwargs):
|
|
146
|
-
sys.stdout.write(' '.join(map(str, args)) + '\n')
|
|
147
|
-
return wrapper
|
|
148
|
-
builtins.print = print_wrapper(builtins.print)
|
|
149
|
-
|
|
150
|
-
# Override logging to maps logs to the correct request
|
|
151
|
-
def logging_wrapper(func, level="info"):
|
|
150
|
+
# Wrap top-level logging helpers to emit structured logs when a request
|
|
151
|
+
# context is available; otherwise fall back to the original behavior.
|
|
152
|
+
def logging_wrapper(func: Callable[..., None], level: str = "info") -> Callable[..., None]:
|
|
152
153
|
@functools.wraps(func)
|
|
153
154
|
def wrapper(*args, **kwargs):
|
|
154
|
-
|
|
155
|
+
try:
|
|
156
|
+
context = storage.get()
|
|
157
|
+
except Exception:
|
|
158
|
+
context = None
|
|
155
159
|
if context is not None:
|
|
156
160
|
send_message({
|
|
157
161
|
"type": "log",
|
|
@@ -168,12 +172,77 @@ if 'VERCEL_IPC_PATH' in os.environ:
|
|
|
168
172
|
func(*args, **kwargs)
|
|
169
173
|
return wrapper
|
|
170
174
|
|
|
171
|
-
logging.basicConfig(level=logging.INFO)
|
|
172
|
-
logging.debug = logging_wrapper(logging.debug)
|
|
173
|
-
logging.info = logging_wrapper(logging.info)
|
|
175
|
+
logging.basicConfig(level=logging.INFO, handlers=[VCLogHandler(send_message, storage.get)], force=True)
|
|
176
|
+
logging.debug = logging_wrapper(logging.debug, "debug")
|
|
177
|
+
logging.info = logging_wrapper(logging.info, "info")
|
|
174
178
|
logging.warning = logging_wrapper(logging.warning, "warn")
|
|
175
179
|
logging.error = logging_wrapper(logging.error, "error")
|
|
176
|
-
logging.
|
|
180
|
+
logging.fatal = logging_wrapper(logging.fatal, "fatal")
|
|
181
|
+
logging.critical = logging_wrapper(logging.critical, "fatal")
|
|
182
|
+
|
|
183
|
+
# Ensure built-in print funnels through stdout wrapper so prints are
|
|
184
|
+
# attributed to the current request context.
|
|
185
|
+
def print_wrapper(func: Callable[..., None]) -> Callable[..., None]:
|
|
186
|
+
@functools.wraps(func)
|
|
187
|
+
def wrapper(*args, **kwargs):
|
|
188
|
+
sys.stdout.write(' '.join(map(str, args)) + '\n')
|
|
189
|
+
return wrapper
|
|
190
|
+
|
|
191
|
+
builtins.print = print_wrapper(builtins.print)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if 'VERCEL_IPC_PATH' in os.environ:
|
|
195
|
+
start_time = time.time()
|
|
196
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
197
|
+
sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
|
|
198
|
+
|
|
199
|
+
send_message = lambda message: sock.sendall((json.dumps(message) + '\0').encode())
|
|
200
|
+
storage = contextvars.ContextVar('storage', default=None)
|
|
201
|
+
|
|
202
|
+
# Override urlopen from urllib3 (& requests) to send Request Metrics
|
|
203
|
+
try:
|
|
204
|
+
import urllib3
|
|
205
|
+
from urllib.parse import urlparse
|
|
206
|
+
|
|
207
|
+
def timed_request(func):
|
|
208
|
+
fetchId = 0
|
|
209
|
+
@functools.wraps(func)
|
|
210
|
+
def wrapper(self, method, url, *args, **kwargs):
|
|
211
|
+
nonlocal fetchId
|
|
212
|
+
fetchId += 1
|
|
213
|
+
start_time = int(time.time() * 1000)
|
|
214
|
+
result = func(self, method, url, *args, **kwargs)
|
|
215
|
+
elapsed_time = int(time.time() * 1000) - start_time
|
|
216
|
+
parsed_url = urlparse(url)
|
|
217
|
+
context = storage.get()
|
|
218
|
+
if context is not None:
|
|
219
|
+
send_message({
|
|
220
|
+
"type": "metric",
|
|
221
|
+
"payload": {
|
|
222
|
+
"context": {
|
|
223
|
+
"invocationId": context['invocationId'],
|
|
224
|
+
"requestId": context['requestId'],
|
|
225
|
+
},
|
|
226
|
+
"type": "fetch-metric",
|
|
227
|
+
"payload": {
|
|
228
|
+
"pathname": parsed_url.path,
|
|
229
|
+
"search": parsed_url.query,
|
|
230
|
+
"start": start_time,
|
|
231
|
+
"duration": elapsed_time,
|
|
232
|
+
"host": parsed_url.hostname or self.host,
|
|
233
|
+
"statusCode": result.status,
|
|
234
|
+
"method": method,
|
|
235
|
+
"id": fetchId
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
return result
|
|
240
|
+
return wrapper
|
|
241
|
+
urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
|
|
242
|
+
except:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
setup_logging(send_message, storage)
|
|
177
246
|
|
|
178
247
|
class BaseHandler(BaseHTTPRequestHandler):
|
|
179
248
|
# Re-implementation of BaseHTTPRequestHandler's log_message method to
|
|
@@ -256,6 +325,7 @@ if 'VERCEL_IPC_PATH' in os.environ:
|
|
|
256
325
|
method()
|
|
257
326
|
self.wfile.flush()
|
|
258
327
|
elif 'app' in __vc_variables:
|
|
328
|
+
# WSGI
|
|
259
329
|
if (
|
|
260
330
|
not inspect.iscoroutinefunction(__vc_module.app) and
|
|
261
331
|
not inspect.iscoroutinefunction(__vc_module.app.__call__)
|
|
@@ -321,10 +391,10 @@ if 'VERCEL_IPC_PATH' in os.environ:
|
|
|
321
391
|
finally:
|
|
322
392
|
if hasattr(response, 'close'):
|
|
323
393
|
response.close()
|
|
394
|
+
# ASGI
|
|
324
395
|
else:
|
|
325
396
|
from urllib.parse import urlparse
|
|
326
397
|
from io import BytesIO
|
|
327
|
-
import asyncio
|
|
328
398
|
|
|
329
399
|
app = __vc_module.app
|
|
330
400
|
|
|
@@ -339,6 +409,7 @@ if 'VERCEL_IPC_PATH' in os.environ:
|
|
|
339
409
|
headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
|
|
340
410
|
else:
|
|
341
411
|
headers_encoded.append([k.lower().encode(), v.encode()])
|
|
412
|
+
|
|
342
413
|
scope = {
|
|
343
414
|
'server': (self.headers.get('host', 'lambda'), self.headers.get('x-forwarded-port', 80)),
|
|
344
415
|
'client': (self.headers.get(
|
|
@@ -361,41 +432,91 @@ if 'VERCEL_IPC_PATH' in os.environ:
|
|
|
361
432
|
else:
|
|
362
433
|
body = b''
|
|
363
434
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
app_queue = asyncio.Queue(loop=loop)
|
|
367
|
-
else:
|
|
368
|
-
app_queue = asyncio.Queue()
|
|
369
|
-
app_queue.put_nowait({'type': 'http.request', 'body': body, 'more_body': False})
|
|
370
|
-
|
|
371
|
-
# Prepare ASGI receive function
|
|
372
|
-
async def receive():
|
|
373
|
-
message = await app_queue.get()
|
|
374
|
-
return message
|
|
375
|
-
|
|
376
|
-
# Prepare ASGI send function
|
|
377
|
-
response_started = False
|
|
378
|
-
async def send(event):
|
|
379
|
-
nonlocal response_started
|
|
380
|
-
if event['type'] == 'http.response.start':
|
|
381
|
-
self.send_response(event['status'])
|
|
382
|
-
if 'headers' in event:
|
|
383
|
-
for name, value in event['headers']:
|
|
384
|
-
self.send_header(name.decode(), value.decode())
|
|
385
|
-
self.end_headers()
|
|
386
|
-
response_started = True
|
|
387
|
-
elif event['type'] == 'http.response.body':
|
|
388
|
-
self.wfile.write(event['body'])
|
|
389
|
-
if not event.get('more_body', False):
|
|
390
|
-
self.wfile.flush()
|
|
435
|
+
# Event to signal that the response has been fully sent
|
|
436
|
+
response_done = threading.Event()
|
|
391
437
|
|
|
392
|
-
#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
438
|
+
# Propagate request context to background thread for logging & metrics
|
|
439
|
+
request_context = storage.get()
|
|
440
|
+
|
|
441
|
+
def run_asgi():
|
|
442
|
+
# Ensure request context is available in this thread
|
|
443
|
+
if request_context is not None:
|
|
444
|
+
token = storage.set(request_context)
|
|
445
|
+
else:
|
|
446
|
+
token = None
|
|
447
|
+
# Track if headers were sent, so we can synthesize a 500 on early failure
|
|
448
|
+
response_started = False
|
|
449
|
+
try:
|
|
450
|
+
async def runner():
|
|
451
|
+
# Per-request app queue
|
|
452
|
+
if _use_legacy_asyncio:
|
|
453
|
+
loop = asyncio.get_running_loop()
|
|
454
|
+
app_queue = asyncio.Queue(loop=loop)
|
|
455
|
+
else:
|
|
456
|
+
app_queue = asyncio.Queue()
|
|
457
|
+
|
|
458
|
+
await app_queue.put({'type': 'http.request', 'body': body, 'more_body': False})
|
|
459
|
+
|
|
460
|
+
async def receive():
|
|
461
|
+
message = await app_queue.get()
|
|
462
|
+
return message
|
|
463
|
+
|
|
464
|
+
async def send(event):
|
|
465
|
+
nonlocal response_started
|
|
466
|
+
if event['type'] == 'http.response.start':
|
|
467
|
+
self.send_response(event['status'])
|
|
468
|
+
if 'headers' in event:
|
|
469
|
+
for name, value in event['headers']:
|
|
470
|
+
self.send_header(name.decode(), value.decode())
|
|
471
|
+
self.end_headers()
|
|
472
|
+
response_started = True
|
|
473
|
+
elif event['type'] == 'http.response.body':
|
|
474
|
+
# Stream body as it is produced; flush on completion
|
|
475
|
+
body_bytes = event.get('body', b'') or b''
|
|
476
|
+
if body_bytes:
|
|
477
|
+
self.wfile.write(body_bytes)
|
|
478
|
+
if not event.get('more_body', False):
|
|
479
|
+
try:
|
|
480
|
+
self.wfile.flush()
|
|
481
|
+
finally:
|
|
482
|
+
response_done.set()
|
|
483
|
+
try:
|
|
484
|
+
app_queue.put_nowait({'type': 'http.disconnect'})
|
|
485
|
+
except Exception:
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
# Run ASGI app (includes background tasks)
|
|
489
|
+
asgi_instance = app(scope, receive, send)
|
|
490
|
+
await asgi_instance
|
|
491
|
+
|
|
492
|
+
asyncio.run(runner())
|
|
493
|
+
except Exception:
|
|
494
|
+
# If the app raised before starting the response, synthesize a 500
|
|
495
|
+
try:
|
|
496
|
+
if not response_started:
|
|
497
|
+
self.send_response(500)
|
|
498
|
+
self.end_headers()
|
|
499
|
+
try:
|
|
500
|
+
self.wfile.flush()
|
|
501
|
+
except Exception:
|
|
502
|
+
pass
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
finally:
|
|
506
|
+
# Always unblock the waiting thread to avoid hangs
|
|
507
|
+
try:
|
|
508
|
+
response_done.set()
|
|
509
|
+
except Exception:
|
|
510
|
+
pass
|
|
511
|
+
if token is not None:
|
|
512
|
+
storage.reset(token)
|
|
513
|
+
|
|
514
|
+
# Run ASGI in background thread to allow returning after final flush
|
|
515
|
+
t = threading.Thread(target=run_asgi, daemon=True)
|
|
516
|
+
t.start()
|
|
517
|
+
|
|
518
|
+
# Wait until final body chunk has been flushed to client
|
|
519
|
+
response_done.wait()
|
|
399
520
|
|
|
400
521
|
if 'Handler' in locals():
|
|
401
522
|
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Auto-generated template used by vercel dev (Python, ASGI)
|
|
2
|
+
# Serves static files from PUBLIC_DIR before delegating to the user ASGI app.
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
import os
|
|
5
|
+
from os import path as _p
|
|
6
|
+
|
|
7
|
+
# Optional StaticFiles import; tolerate missing deps
|
|
8
|
+
StaticFiles = None
|
|
9
|
+
try:
|
|
10
|
+
from fastapi.staticfiles import StaticFiles as _SF
|
|
11
|
+
StaticFiles = _SF
|
|
12
|
+
except Exception:
|
|
13
|
+
try:
|
|
14
|
+
from starlette.staticfiles import StaticFiles as _SF
|
|
15
|
+
StaticFiles = _SF
|
|
16
|
+
except Exception:
|
|
17
|
+
StaticFiles = None
|
|
18
|
+
|
|
19
|
+
USER_MODULE = "__VC_DEV_MODULE_PATH__"
|
|
20
|
+
_mod = import_module(USER_MODULE)
|
|
21
|
+
_app = getattr(_mod, 'app', None)
|
|
22
|
+
if _app is None:
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
f"Missing 'app' in module '{USER_MODULE}'. Define `app = ...` (ASGI app)."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Sanic compatibility: prefer `app.asgi` when available
|
|
28
|
+
USER_ASGI_APP = getattr(_app, 'asgi', _app)
|
|
29
|
+
|
|
30
|
+
PUBLIC_DIR = 'public'
|
|
31
|
+
|
|
32
|
+
# Prepare static files app (if starlette/fastapi installed)
|
|
33
|
+
static_app = None
|
|
34
|
+
if StaticFiles is not None:
|
|
35
|
+
try:
|
|
36
|
+
try:
|
|
37
|
+
static_app = StaticFiles(directory=PUBLIC_DIR, check_dir=False)
|
|
38
|
+
except TypeError:
|
|
39
|
+
# Older Starlette without check_dir parameter
|
|
40
|
+
static_app = StaticFiles(directory=PUBLIC_DIR)
|
|
41
|
+
except Exception:
|
|
42
|
+
static_app = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def app(scope, receive, send):
|
|
46
|
+
if static_app is not None and scope.get('type') == 'http':
|
|
47
|
+
req_path = scope.get('path', '/') or '/'
|
|
48
|
+
safe = _p.normpath(req_path).lstrip('/')
|
|
49
|
+
full = _p.join(PUBLIC_DIR, safe)
|
|
50
|
+
try:
|
|
51
|
+
base = _p.realpath(PUBLIC_DIR)
|
|
52
|
+
target = _p.realpath(full)
|
|
53
|
+
if (target == base or target.startswith(base + _p.sep)) and _p.isfile(target):
|
|
54
|
+
await static_app(scope, receive, send)
|
|
55
|
+
return
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
await USER_ASGI_APP(scope, receive, send)
|