Enviar imagens de um editor de texto rico no Razor Pages

Em um aplicativo Razor Pages, há dois POSTs diferentes em torno do editor. Cada imagem é enviada no momento em que é inserida, para um endpoint que retorna a URL dela. Depois, o formulário da própria página envia o HTML final, com essas URLs, para OnPost. Esta página mostra os dois e por que eles ficam separados: o formulário mantém o token antiforgery, e o HTML fica pequeno o bastante para os limites de formulário do Razor Pages.
O formulário
<form method="post" id="post">
<input type="hidden" asp-for="Html" />
<div id="editor"></div>
<p><button type="submit">Save</button> @Model.Message</p>
</form>
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
});
var html = document.getElementById("Html");
editor.setHTMLCode(html.value);
document.getElementById("post").addEventListener("submit", function () {
html.value = editor.getHTMLCode();
});
</script><input type="hidden" asp-for="Html" />leva o HTML. A tag helper<form method="post">adiciona sozinha o campo oculto__RequestVerificationToken.- O editor começa com o valor do campo oculto, então, depois de um POST, a página mostra de novo o conteúdo salvo.
- O handler de submit copia
editor.getHTMLCode()para o campo logo antes de o formulário ser enviado.
O page model
// Razor Pages validates the antiforgery token on this POST by default; the <form> tag helper
// in Index.cshtml adds the hidden __RequestVerificationToken field. Save Html to your database here.
public void OnPost() => Message = $"Saved {Html?.Length ?? 0} characters of HTML.";[BindProperty] em Html faz o binding do campo no POST. O Razor Pages valida o token antiforgery antes de OnPost rodar; um POST sem ele recebeu um 400. Sanitize o HTML no servidor antes de armazená-lo ou exibi-lo a outros usuários: o que o editor envia continua sendo entrada vinda do navegador.
Por que as imagens são enviadas primeiro
Sem um handler de upload, o editor incorpora uma imagem colada no HTML como uma URL data: em base64, que é um texto longo. Um valor de formulário do Razor Pages é limitado por FormOptions.ValueLengthLimit, 4,194,304 caracteres por padrão. Com um token válido, um POST de formulário de 100,000 caracteres foi salvo, e um de 4,300,000 caracteres recebeu um 400 antes de OnPost rodar. Com a rota de upload, o HTML guarda apenas uma URL curta por imagem. Depois de inserir uma imagem enviada em um editor vazio e clicar em Save, a página informou "Saved 108 characters of HTML."
A rota de upload
O rte-files.js, carregado na página, envia todas as imagens para /rte/upload: arrastar e soltar, colar, a caixa de diálogo Insert Image e o botão Upload da galeria. A rota é um endpoint de Minimal API mapeado em Program.cs com app.MapRteFiles():
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);Ela não pode ser um handler de página, porque handlers são acessados como /?handler=Upload e todo handler POST exige o token antiforgery, que a XHR do editor não consegue enviar. Adicionar uma galeria de imagens a um editor de texto rico no Razor Pages explica essa escolha. Os dois limites mantêm a requisição abaixo de 4,065,536 bytes; caso contrário, o Kestrel aceitaria 30,000,000. Em seguida, RteFiles.Save verifica a origem, a pasta, o tamanho, a extensão e os primeiros bytes, e armazena o arquivo com um novo nome. Essas verificações são as mesmas do guia de upload do ASP.NET Core.

Teste
Execute o projeto de exemplo deste guia com dotnet run e abra http://localhost:5303. Insira uma imagem da galeria, envie uma com o botão Upload dela se quiser, e clique em Save: a mensagem mostra o tamanho do HTML, e a imagem continua lá depois que a página recarrega. Em um shell:
curl -F "file=@photo.jpg" http://localhost:5303/rte/upload
{"url":"/uploads/photo-4cdaeb3e.jpg"}
curl -s -o /dev/null -w "%{http_code}\n" -d "Html=x" http://localhost:5303/
400O upload funciona sem token. O POST de formulário puro é recusado, porque não tem nenhum.
O mesmo guia para outras plataformas
JavaScript · Next.js · PHP · ASP.NET Core · Blazor · MVC 5 · Web Forms