Save rich text editor content with fetch in ASP.NET Core

A full page reload interrupts someone who is writing. To save in the background, read the editor's HTML in JavaScript and send it with fetch. This guide shows how, and the two things that are easy to get wrong.
Get the editor in JavaScript
Each editor registers itself in window.richTextBoxEditors under its id or name. Read its HTML with getHTMLCode():
<richtextbox id="ArticleEditor" name="ArticleEditor" toolbar="default" height="400px"></richtextbox> <button type="button" id="save">Save</button> <span id="status"></span>
document.getElementById("save").addEventListener("click", async function () {
var editor = window.richTextBoxEditors.ArticleEditor;
var token = document.querySelector("input[name=__RequestVerificationToken]").value;
var response = await fetch("/api/articles/42", {
method: "POST",
headers: { "Content-Type": "application/json", "RequestVerificationToken": token },
body: JSON.stringify({ html: editor.getHTMLCode() })
});
document.getElementById("status").textContent = response.ok ? "Saved." : "Could not save.";
});
The token line needs a form on the page, or @Html.AntiForgeryToken(), to write that hidden input.
The endpoint
app.MapPost("/api/articles/{id:int}", async (int id, SaveRequest request,
IRichTextBoxHtmlSanitizer sanitizer, AppDbContext db, IAntiforgery antiforgery, HttpContext context) =>
{
await antiforgery.ValidateRequestAsync(context);
var article = await db.Articles.FindAsync(id);
if (article is null) return Results.NotFound();
article.Body = sanitizer.Sanitize(request.Html);
await db.SaveChangesAsync();
return Results.Ok();
}).RequireAuthorization();
record SaveRequest(string Html);
The two things to get right
- Clean it on the server.
[RichTextHtml]runs in form model binding, and a JSON API does not go through that. CallIRichTextBoxHtmlSanitizeryourself, as above. Otherwise this endpoint is an open door for scripts. - Keep the antiforgery check. Minimal API endpoints that read JSON do not validate it for you, so another site could make a signed-in user's browser post here if it could send the request. The call above checks it.
Do not lose work
A failed request should tell the user, and keep their text in the editor. For protection against a closed tab, add autosave as well: it keeps a draft in the browser until you have saved.