Handling HTTP in Mithril with m.request and manual redraws
Use m.request for declarative HTTP calls in Mithril, handle non-JSON responses, and redraw background requests manually.
21 Aug 2025, 19:07 UTC

The stale-profile problem
You mount a Mithril component, call an API in oninit, and the view never updates even though the network request succeeds. Or the request fails because the server returns plain text and Mithril tries to parse it as JSON. These are common friction points when using m.request, Mithril's built-in HTTP helper.
The useful takeaway: m.request is a promise-based wrapper around the browser's XMLHttpRequest or fetch. It automatically parses JSON and sets headers, but it does not retry failed requests, cache responses, or redraw the UI when you opt out of automatic rendering. You control those behaviors through the request options and Mithril's lifecycle.
How m.request processes a response
Mithril 2.x provides m.request as a declarative alternative to manual fetch calls. You pass a configuration object with method, url, data, and headers. When data is present, Mithril serializes it to JSON and sets Content-Type: application/json. On success, the promise resolves with the parsed response body. On failure, it rejects with an error object.
Because it returns a thenable, you can chain .then and .catch or use async/await inside lifecycle hooks such as oninit or oncreate. A thenable is any object with a then method, so it works with standard promise code.
Non-JSON responses and custom deserialization
By default, m.request runs JSON.parse on the response text. If your endpoint returns plain text, HTML, or an empty body with a 204 status, that parse step throws and the promise rejects. Fix this with the deserialize option, which replaces the default JSON parser.
Deserialize means converting raw response text into a JavaScript value. Setting deserialize: (value) => value passes the text through unchanged.
// Run in a browser with Mithril 2.x loaded.
// Replace ENDPOINT with your actual URL.
m.request({
method: "GET",
url: ENDPOINT,
deserialize: (value) => value, // treat response as plain text
})
.then((text) => {
console.log("Raw response:", text);
})
.catch((err) => {
console.error("Request failed:", err);
});
Expected check: open the Network tab in browser dev tools, confirm the response status and body, then verify the .then callback receives the raw string instead of throwing a syntax error.
Background requests require manual redraws
By default, Mithril redraws the UI automatically when an m.request promise settles. Setting background: true skips that automatic redraw, which is useful for polling or non-critical updates. The trade-off is that the screen stays stale until you call m.redraw() yourself.
m.redraw() tells Mithril to re-evaluate components and update the DOM.
// Run in a Mithril 2.x component.
// Replace API_BASE and USER_ID with your values.
const API_BASE = "https://api.example.com";
const USER_ID = "42";
m.mount(document.body, {
oninit: function () {
this.user = null;
this.error = null;
m.request({
method: "GET",
url: `${API_BASE}/users/${USER_ID}`,
background: true,
})
.then((data) => {
this.user = data;
m.redraw(); // required because background: true
})
.catch((err) => {
this.error = err;
m.redraw();
});
},
view: function () {
if (this.error) return m("p", { class: "error" }, "Failed to load profile.");
if (!this.user) return m("p", "Loading...");
return m("div", [
m("h1", this.user.name),
m("p", this.user.email),
]);
},
});
Risks: forgetting m.redraw() leaves the component showing "Loading..." indefinitely. Unhandled rejections can surface in the console; always attach a .catch.
Limitation: no built-in retry or cache
m.request does not automatically retry on network errors or cache responses for offline use. If you need retries, wrap the call in a small helper or use a dedicated retry library. For caching, store the resolved value in a Mithril stream or plain variable and check it before issuing a new request.
A practical way to check the result: after mounting the component, open dev tools, filter the Network panel by your API URL, and confirm exactly one request fires. Then disable the network and reload; without a custom cache layer, the component should show an error or loading state rather than old data.
Closing
Use m.request when you want declarative HTTP calls that fit naturally inside Mithril components. Provide deserialize for non-JSON APIs, set background: true only when you are prepared to call m.redraw(), and implement retry or caching yourself if your application requires it. Verify behavior in the Network tab and console before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.