Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/dstack/_internal/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from fastapi import Depends, FastAPI, Request, Response, status
from fastapi.datastructures import URL
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from packaging.version import Version

from dstack._internal import settings as core_settings
Expand Down Expand Up @@ -73,6 +72,7 @@
from dstack._internal.server.utils.logging import configure_logging
from dstack._internal.server.utils.routers import (
CustomORJSONResponse,
CustomStaticFiles,
check_client_server_compatibility,
error_detail,
get_client_version,
Expand Down Expand Up @@ -366,7 +366,7 @@ async def healthcheck():

if ui and Path(__file__).parent.joinpath("statics").exists():
app.mount(
"/", StaticFiles(packages=["dstack._internal.server"], html=True), name="statics"
"/", CustomStaticFiles(packages=["dstack._internal.server"], html=True), name="statics"
)

@app.exception_handler(404)
Expand Down
17 changes: 17 additions & 0 deletions src/dstack/_internal/server/utils/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,30 @@
import orjson
import packaging.version
from fastapi import HTTPException, Request, Response, status
from fastapi.staticfiles import StaticFiles

from dstack._internal.core.errors import ServerClientError, ServerClientErrorCode
from dstack._internal.core.models.common import CoreModel
from dstack._internal.utils.json_utils import get_orjson_default_options, orjson_default
from dstack._internal.utils.version import parse_version


class CustomStaticFiles(StaticFiles):
"""
StaticFiles raises AssertionError on "websocket" scope type,
but starlette's Mount() matches both "http" and "websocket".
So a custom ASGI app is needed to reject WebSocket requests gracefully.

See: https://github.com/dstackai/dstack/issues/4061
"""

async def __call__(self, scope, receive, send) -> None:
if scope["type"] == "websocket":
await send({"type": "websocket.close"}) # Reject the handshake
return
await super().__call__(scope, receive, send)


class CustomORJSONResponse(Response):
"""
Custom JSONResponse that uses orjson for serialization.
Expand Down
Loading