Skip UseFileServer when wwwroot is missing.

Local Aspire has no published client folder; the middleware warned on every boot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 13:22:28 +03:00
co-authored by Cursor
parent 782a56eefe
commit e35b8e438c
5 changed files with 81 additions and 1 deletions
+37
View File
@@ -0,0 +1,37 @@
# Баг 4. UseFileServer без каталога wwwroot
## Симптом
При локальном старте сервера (Aspire, без publish):
```
warn: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[16]
The WebRootPath was not found: …\src\HSchool.Server\wwwroot. Static files may be unavailable.
```
Клиент в деве идёт с Vite на 5173, не с сервера. Ожидали отсутствие этого предупреждения.
## Причина
`Program` всегда зовёт `UseFileServer()`. ASP.NET подставляет `ContentRoot/wwwroot` даже если папки нет и пишет warning 16. Каталог появляется только в опубликованном контейнере (`PublishWithContainerFiles(client, "wwwroot")`).
## Путь
`UseFileServer` только если `wwwroot` есть на диске. Отвергнуто: пустой каталог с `.gitkeep`; флаг `IsDevelopment` (publish с Development тогда не раздаст клиент).
## Задачи
- [x] Не вызывать `UseFileServer`, пока `WebRootPath` не существует
- [x] Тест предиката на существующий и отсутствующий каталог
## Тест, без которого не закрыт
`HSchool.Server.Tests`: `ClientStaticFiles.CanServe` ложь на несуществующем пути, истина на живом каталоге.
```bash
dotnet test tests/HSchool.Server.Tests --filter FullyQualifiedName~ClientStaticFiles
```
## Стоп
Протокол, сейв, Vite, `PublishWithContainerFiles` не трогать.
+1
View File
@@ -12,3 +12,4 @@
| [1. Окно входа](01-login-layout.md) | ✅ | Форма входа прижата к углу, поле пароля на всю ширину |
| [2. Запуск AppHost](02-client-endpoint-proxy.md) | ✅ | `run-aspire` падает: proxy + Port=TargetPort=5173 у client |
| [3. users.json как сейв](03-users-json-as-school.md) | ✅ | При старте: `Save …/users.json has id 0; ids start at 1` |
| [4. Нет wwwroot](04-wwwroot-not-found.md) | 🔄 | При старте: `The WebRootPath was not found: …/wwwroot` |
+10
View File
@@ -0,0 +1,10 @@
namespace HSchool.Server;
/// <summary>
/// Published images copy the Vite build into wwwroot. Local Aspire has no such folder.
/// </summary>
internal static class ClientStaticFiles
{
public static bool CanServe(string? webRootPath) =>
!string.IsNullOrEmpty(webRootPath) && Directory.Exists(webRootPath);
}
+5 -1
View File
@@ -159,7 +159,11 @@ app.Map("/ws/game", async (HttpContext context, GameSocketHandler handler, Sessi
app.MapDefaultEndpoints();
// In a published container the built client lands in wwwroot next to the server.
app.UseFileServer();
// Local Aspire has no such folder — Vite serves the UI, and UseFileServer would warn.
if (ClientStaticFiles.CanServe(app.Environment.WebRootPath))
{
app.UseFileServer();
}
app.Run();
@@ -0,0 +1,28 @@
namespace HSchool.Server.Tests;
public class ClientStaticFilesTests
{
[Fact]
public void CanServe_IsFalseWhenTheDirectoryIsMissing()
{
var missing = Path.Combine(Path.GetTempPath(), "h-school-no-wwwroot-" + Guid.NewGuid().ToString("N"));
Assert.False(ClientStaticFiles.CanServe(missing));
Assert.False(ClientStaticFiles.CanServe(null));
Assert.False(ClientStaticFiles.CanServe(""));
}
[Fact]
public void CanServe_IsTrueWhenTheDirectoryExists()
{
var directory = Directory.CreateTempSubdirectory("h-school-wwwroot-");
try
{
Assert.True(ClientStaticFiles.CanServe(directory.FullName));
}
finally
{
directory.Delete(recursive: true);
}
}
}