Upload images from a rich text editor in Razor Pages

In a Razor Pages app there are two different posts around the editor. Each picture is uploaded the moment it is inserted, to an endpoint that returns its URL. Later, the page's own form posts the finished HTML, with those URLs in it, to OnPost. This page shows both and why they are kept apart: the form keeps its antiforgery token, and the HTML stays small enough for Razor Pages' form limits.
The form
<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" />carries the HTML. The<form method="post">tag helper adds the hidden__RequestVerificationTokenfield on its own.- The editor starts from the hidden field's value, so after a post the page shows the saved content again.
- The submit handler copies
editor.getHTMLCode()into the field right before the form is sent.
The 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] on Html binds the field on POST. Razor Pages validates the antiforgery token before OnPost runs; a post without it got a 400. Sanitize the HTML on the server before you store it or show it to other users: what the editor sends is still input from the browser.
Why the pictures are uploaded first
Without an upload handler, the editor embeds a pasted picture in the HTML as a base64 data: URL, which is long text. A Razor Pages form value is limited by FormOptions.ValueLengthLimit, 4,194,304 characters by default. With a valid token, a form post of 100,000 characters was saved, and one of 4,300,000 characters got a 400 before OnPost ran. With the upload route, the HTML holds only a short URL per picture. After inserting an uploaded picture into an empty editor and clicking Save, the page reported "Saved 108 characters of HTML."
The upload route
rte-files.js, loaded on the page, sends every picture to /rte/upload: drag and drop, paste, the Insert Image dialog and the gallery's Upload button. The route is a Minimal API endpoint mapped in Program.cs with 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);It cannot be a page handler, because handlers are addressed as /?handler=Upload and every POST handler requires the antiforgery token, which the editor's XHR cannot send. Add an image gallery to a rich text editor in Razor Pages explains that choice. The two limits keep the request under 4,065,536 bytes; Kestrel would otherwise accept 30,000,000. RteFiles.Save then checks the origin, the folder, the size, the extension and the first bytes, and stores the file under a new name. Those checks are the same as in the ASP.NET Core upload guide.

Test it
Run the sample project for this guide with dotnet run and open http://localhost:5303. Insert a picture from the gallery, upload one with its Upload button if you like, and click Save: the message shows the length of the HTML, and the picture is still there after the page reloads. From a 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/
400The upload works without a token. The bare form post is refused, because it has none.
The same guide for other platforms
JavaScript · Next.js · PHP · ASP.NET Core · Blazor · MVC 5 · Web Forms