Subir imágenes desde un editor de texto enriquecido en Blazor

Diagrama: Home.razor crea el editor mediante JS interop, y el editor envía las solicitudes de la galería y de carga por HTTP simple a endpoints de Program.cs, no por el circuito SignalR

En Blazor Server es tentador enviarlo todo a través del componente: el circuito ya está conectado y C# está ahí mismo. Para las cargas de imágenes del editor eso no funciona, y los motivos son propios de Blazor. Esta página explica por qué las imágenes van a endpoints HTTP normales en la misma aplicación, cómo encajan esos endpoints junto a MapRazorComponents y qué les hace el middleware antiforgery de la plantilla de Blazor.

Por qué no el circuito

  • El editor envía a una URL. Arrastrar y soltar, pegar, el cuadro de diálogo Insert Image y el botón Upload de la galería envían todos un XHR multipart/form-data, a través de rte-files.js, a /rte/upload. Un componente Razor no tiene ninguna URL que acepte un POST.
  • El circuito tiene un límite de mensaje de 32 KB. Los mensajes del navegador al servidor por SignalR están limitados a 32 KB de forma predeterminada, y eso incluye los resultados de JS interop. Devolvimos el HTML del editor como cadena mediante InvokeAsync<string>: 20,000 caracteres llegaron; 40,000 cerraron el circuito con "Connection closed with an error". Pasar los bytes de las imágenes por interop chocaría con el mismo muro. El propio <InputFile> de Blazor lo sortea con streaming, pero el editor no usa InputFile.

Así que el componente solo crea el editor (Añadir una galería de imágenes a un editor de texto enriquecido en Blazor muestra cómo), y los archivos viajan por HTTP, como en cualquier otra aplicación 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() asigna tres endpoints de Minimal API: GET y POST /rte/gallery y POST /rte/upload. UseRteStaticFiles() sirve las imágenes guardadas en /uploads, el editor en /richtexteditor y rte-files.js. Todo se ejecuta en el mismo proceso que el circuito, así que los endpoints pueden usar los mismos servicios y el mismo inicio de sesión.

El antiforgery de la plantilla

La plantilla de Blazor Web App llama a app.UseAntiforgery() para los formularios propios de Blazor. En .NET 8, un endpoint que enlaza IFormFile lleva metadatos antiforgery, así que con ese middleware activo, una carga sin token recibió un 400. En Development se ve el motivo: "Invalid anti-forgery token found when reading parameter "IFormFile file" from the request body as form." Los XHR del editor no llevan token, así que los dos endpoints POST quedan excluidos:

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);

En lugar del token, RteFiles rechaza los POST que un navegador marca como procedentes de otro sitio, usando Sec-Fetch-Site y Origin. Los formularios propios de Blazor conservan su protección. La ruta de carga también reduce el límite de la solicitud a 4,065,536 bytes, frente a los 30,000,000 predeterminados de Kestrel; Límites de tamaño de carga en ASP.NET Core explica qué ocurre por encima. Después, RteFiles.Save comprueba la carpeta, el tamaño, la extensión y los primeros bytes, y guarda el archivo con un nombre nuevo, como en la guía de carga para ASP.NET Core.

RichTextEditor en el ejemplo de Blazor con una foto subida de una ciudad de noche insertada en el texto y seleccionada, con la barra de herramientas completa encima
Una imagen de la galería, insertada en la página de Blazor. Su URL vino del endpoint HTTP.

Pruébalo

Ejecuta el proyecto de ejemplo de esta guía con dotnet run y abre http://localhost:5304. Abre Insert Gallery, haz clic en Upload y elige una foto: se guarda, aparece en la lista y se inserta al hacer doble clic en ella. Desde un shell, la misma ruta:

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

En el panel de red del navegador, la imagen es una solicitud POST /rte/upload normal junto al WebSocket _blazor.

La misma guía para otras plataformas

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

Para seguir leyendo