Téléverser des images depuis un éditeur de texte enrichi avec Blazor

Schéma : Home.razor crée l’éditeur via l’interopérabilité JS, et l’éditeur envoie les requêtes de galerie et d’envoi en HTTP simple à des points de terminaison de Program.cs, et non par le circuit SignalR

Avec Blazor Server, il est tentant de tout faire passer par le composant : le circuit est déjà connecté, et le C# est juste là. Pour les envois d’images de l’éditeur, cela ne fonctionne pas, et les raisons sont propres à Blazor. Cette page explique pourquoi les images vont vers de simples points de terminaison HTTP dans la même application, comment ces points de terminaison s’articulent avec MapRazorComponents, et ce que leur fait le middleware antifalsification du modèle de projet Blazor.

Pourquoi pas le circuit

  • L’éditeur envoie vers une URL. Le glisser-déposer, le collage, la boîte de dialogue Insert Image et le bouton Upload de la galerie envoient tous une XHR multipart/form-data, via rte-files.js, vers /rte/upload. Un composant Razor n’a pas d’URL qui accepte un POST.
  • Les messages du circuit sont limités à 32 Ko. Les messages du navigateur vers le serveur par SignalR sont limités par défaut à 32 Ko, y compris les résultats d’interopérabilité JS. Nous avons renvoyé le HTML de l’éditeur sous forme de chaîne via InvokeAsync<string> : 20,000 caractères sont arrivés, 40,000 ont fermé le circuit avec « Connection closed with an error ». Faire passer les octets d’une image par l’interopérabilité se heurterait au même mur. Le composant <InputFile> de Blazor contourne cette limite par streaming, mais l’éditeur n’utilise pas InputFile.

Le composant se contente donc de créer l’éditeur (Ajouter une galerie d’images à un éditeur de texte enrichi avec Blazor montre comment), et les fichiers transitent en HTTP, comme dans n’importe quelle autre application ASP.NET Core.

Program.cs

C# · Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();

app.UseStaticFiles();
app.UseRteStaticFiles();   // /uploads, /richtexteditor, /rte-files.js
app.UseAntiforgery();      // Blazor's own forms; the /rte endpoints opt out (RteEndpoints.cs)

app.MapRteFiles();         // GET+POST /rte/gallery, POST /rte/upload
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();

MapRteFiles() déclare trois points de terminaison Minimal API : GET et POST /rte/gallery, et POST /rte/upload. UseRteStaticFiles() sert les images stockées sous /uploads, l’éditeur sous /richtexteditor, ainsi que rte-files.js. Tout cela s’exécute dans le même processus que le circuit : les points de terminaison peuvent donc utiliser les mêmes services et la même connexion.

L’antifalsification du modèle de projet

Le modèle Blazor Web App appelle app.UseAntiforgery(), pour les formulaires propres à Blazor. En .NET 8, un point de terminaison qui lie IFormFile porte des métadonnées antifalsification : avec ce middleware en place, un envoi sans jeton a donc reçu un 400. En Development, la raison est visible : « Invalid anti-forgery token found when reading parameter "IFormFile file" from the request body as form. ». Les XHR de l’éditeur ne portent pas de jeton : les deux points de terminaison POST se soustraient donc à cette vérification :

C# · RteEndpoints.cs
// Binding [FromForm] or IFormFile makes .NET 8 demand an antiforgery token on these endpoints.
// The editor's XHRs cannot send one, so it is switched off here and RteFiles checks the
// request's origin instead (see RequireSameOrigin). guide-notes.md has the details.
rte.MapPost("/gallery", (HttpContext http, [FromForm] string? action, [FromForm] string? folder, [FromForm] string? name) =>
    Json(RteFiles.CreateFolder(http, action, folder, name)))
    .DisableAntiforgery();
C# · RteEndpoints.cs
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
    Json(await RteFiles.Save(http, file, folder)))
    .DisableAntiforgery()
    // Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
    .WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
    .WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);

À la place du jeton, RteFiles refuse les POST qu’un navigateur signale comme provenant d’un autre site, à l’aide de Sec-Fetch-Site et Origin. Les formulaires propres à Blazor conservent leur protection. La route d’envoi abaisse aussi la limite de requête à 4,065,536 octets, contre 30,000,000 par défaut pour Kestrel ; Limites de taille d’envoi avec ASP.NET Core décrit ce qui se passe au-delà. Ensuite, RteFiles.Save vérifie le dossier, la taille, l’extension et les premiers octets, et stocke le fichier sous un nouveau nom, comme dans le guide sur l’envoi avec ASP.NET Core.

RichTextEditor dans l’exemple Blazor, avec une photo envoyée d’une ville de nuit insérée dans le texte et sélectionnée, et la barre d’outils complète au-dessus
Une image de la galerie, insérée dans la page Blazor. Son URL provient du point de terminaison HTTP.

Tester

Lancez le projet d’exemple de ce guide avec dotnet run et ouvrez http://localhost:5304. Ouvrez Insert Gallery, cliquez sur Upload et choisissez une photo : elle est stockée, listée, puis insérée quand vous double-cliquez dessus. Depuis un shell, la même route :

Shell
curl -F "file=@photo.jpg" http://localhost:5304/rte/upload
{"url":"/uploads/photo-e2f78227.jpg"}

Dans le panneau Réseau du navigateur, l’image est une requête POST /rte/upload normale, à côté du WebSocket _blazor.

Le même guide pour d’autres plateformes

JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · MVC 5 · Web Forms

À lire ensuite