Fetch will reject your GET if it contains a body (a deliberate maintainer decision), even though it's entirely permissible by HTTP and done by many real-world AJAX APIs. Real AJAX will do what it's supposed to. (The HTTP 1.1 2014 Spec says that including a request body in a GET "might cause some implementations to reject the request." Guess which one!)
Also, advanced features like progress are completely absent from Fetch as well.
However, there are some fantastic libraries like Axios[1], SuperAgent (requires npm), and, yes, jQuery[2], that have really excellent API's (far superior to Fetch), or you could just write your own (or use an LLM) short wrapper around modern AJAX and call it a day. h/t to Claude:
const xhr = ['GET','POST','PUT','PATCH','DELETE'].reduce((x,m) => (x[m.toLowerCase()] =
(u,d,opt={}) => new Promise((r,j) => {
const q = new XMLHttpRequest();
q.open(m,u);
q.responseType = opt.responseType || '';
if(opt.headers) Object.entries(opt.headers).forEach(([k,v]) => q.setRequestHeader(k,v));
if(opt.signal) opt.signal.addEventListener('abort', () => q.abort());
q.withCredentials = opt.credentials === 'include';
q.onload = () => r({
ok: q.status >= 200 && q.status < 300,
status: q.status,
headers: new Headers(q.getAllResponseHeaders()),
text: () => Promise.resolve(q.responseText),
json: () => Promise.resolve(JSON.parse(q.responseText)),
blob: () => Promise.resolve(new Blob([q.response])),
response: q
});
q.onerror = () => j(new TypeError('Network request failed'));
q.send(d instanceof FormData ? d : JSON.stringify(d));
}), x), {});
This gives you xhr methods with a fetch-style API and you can still do all the things that fetch can't (but this won't do real streaming or cache control like Fetch, but it'll do 95% of all common use cases in a tiny bit of code.)Each method listed above returns a Promise that resolves with the XMLHttpRequest object or rejects with the error. So you get both the Promise functionality and full access to the XHR object in the resolution.
Usage:
xhr.post('/api', { data: 123 }, {
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
signal: abortController.signal
})
.then(res => res.json())
.then(data => console.log(data));
For more advanced AJAX stuff, check out the very powerful and flexible Axios library[1].And, if you don't need AJAX but do want some of the features from jQuery (like some of the more unusual selectors) that aren't in Cash (to save bytes!), AJAX (and special effects) is excluded from jQuery Slim which brings the code down to only 69KB[3].
1. Axios https://github.com/axios/axios (41kb)
2. jQuery AJAX https://api.jquery.com/jQuery.ajax/ (87kb but includes ALL of jquery!)
In standard HTTP/1.1, any method can have a request body. In Representational State Transfer (REST) as defined by Dr. Fielding, HTTP doesn't even come up, let alone "methods" per se, so there is no distinction between DELETE, POST, or GET from a REST standpoint, only within HTTP as an engine for hypertext. Further, in HTTP, any of these requests can contain a request body.
But, because of this behavior by the WhatWG for Fetch, the IETF has added this paragraph to the specification for HTTP/1.1:
"A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request."
"Some existing implementations" really just means fetch. The p*ing contest between two groups resulted in a neutered and prescriptive fetch.In other words, it's fetch that is non-standard, and the actual HTTP standard had to be updated to let you know that.
Since then, each iteration of the HTTP specs has strengthened the advice. The most recent 9110 family says you SHOULD NOT use GET request bodies unless you have confirmed in some way that they'll work, because otherwise you can't trust they'll work.
Fetch was going along with this consensus, not causing the problem.
The pool was muddied; nay, poisoned. And so the solution is the QUERY method. That's how things tend to work in such a space. See also 307 because of 302 being misimplemented.