You need to know that when you write an HTTP-based API, you have to make all your GET requests idempotent. Idempotent is a fancy word which usually means ‘read-only.’ If you repeat an idempotent function any number of times, you should always get the same result.
When you write an HTTP API, you absolutely do have to make sure your GET requests don’t do anything but read & response with existing data. The reason for this is not superstition. Nor is it there to help us neatly organize our HTTP verbs — our industry has completely ignored HTTP verbs in network APIs before (SOAP), and they will again, (GraphQL, gRPC) because HTTP is a bad way to make an API (REST.) HTTP and all its trappings are made for web browsers’ navigation, static file access, and resource loading; the protocol is ill-designed for modern API needs.
That note about web browsers, though, is precisely why you shouldn’t use the GET verb on any requests which mutate state. Browsers mindlessly fire off GET requests for any hyperlinks the end user clicks, as well as silently firing off GETs for static resource such as <img> tags. Historically this has been a very, very bad security problem for the Web, because it essentially allows any website to hijack a user’s browser to send unwanted requests. From a security perspective, it’s a tragedy that the web ever took off, because the entire browser/Html/HTTP system is essentially based on a conflation of code and data. The browser is a remote code execution machine. (For comparison, consider mobile apps have no such problems like XSS/CSRF that were — and still are — pandemics on the Web.)
It’s much easier for an attacker to trick a user’s browser into firing off GET requests than any other HTTP verb. For that reason, you should be very careful about what your GET endpoints do. You should probably leave them as read-only.
But just because your endpoints are read-only, doesn’t mean they’re idempotent. If your server manages state (which they nearly all do) there’s no such thing as an idempotent API. Simply put, idempotent is the wrong word.
In a client-server situation, idempotence is an implementation detail on the server. From the client’s perspective, there can be no such thing as idempotence with respect to server-controlled state.
I’ve had this debate many times, because some programmers view it as a fundamental law that GET requests are/should be idempotent. It must be quite jarring for me to suggest that such a thing is not merely wrong, but in fact incomprehensible nonsense. I once had a programmer say this to me as their argument:
I write my GET methods to be idempotent. This is great because no matter how many times I call GET on that resource, I know I’m not changing anything and I’ll always get back the same response.
Actually, the client of a GET request has no guarantee they will get back the same response. That’s the problem.
Consider this hypothetical sequence of calls which could totally happen in a real-life grocery list app:
// GET /grocies/list is a 100% read-only implementation
Request: ---> HTTP GET /groceries/list Response: <--- [ 'eggs', 'milk' ]
Request: ---> HTTP GET /groceries/list Response: <--- [ 'eggs', 'milk', 'butter' ]
Request: ---> HTTP GET /groceries/list Response: <--- [ 'eggs', 'milk', 'butter', 'bread']
“What?!” you protest. “If the GET was made properly, why does it append a new item to the grocery list each time it’s called? That’s not idempotent!” Exactly. But this exact interaction can and does happen in APIs even when they are designed to have idempotent GET requests. It happens constantly, every day, in basically every client-server API ever, ever. Here’s why.
Again, idempotence is a server’s private implementation detail. It’s the server’s business and nobody else needs to know or care. From the client’s perspective, the concept is complete nonsense. Here’s a more complete picture of the above exchange:
(You) Request: ---> HTTP GET /groceries/list Response: <--- [ 'eggs', 'milk' ]
(Your spouse) Request: ---> HTTP POST /groceries/list/?append=butter Response: <--- [ 'eggs', 'milk', 'butter' ]
(You) Request: ---> HTTP GET /groceries/list Response: <--- [ 'eggs', 'milk', 'butter' ]
(Your spouse) Request: ---> HTTP POST /groceries/list/?append=bread Response: <--- [ 'eggs', 'milk', 'butter', 'bread']
(You) Request: ---> HTTP GET /groceries/list Response: <--- [ 'eggs', 'milk', 'butter', 'bread']
Unbeknownst to you, another client had sent POST requests to the same resource. From your perspective, all you could see was the server’s state infuriatingly changing between your GET requests.
As long as the server controls mutable state, the client never has any concept of whether or not their GET requests are mutating that state.
The client will always have to handle different results on subsequent GET requests. It has no way to know what causes the change and whether or not it was the GET request itself which affected that change. There’s no way to know and no reason to care. It’s not that the GET method is or isn’t idempotent. It’s that the very concept of idempotence does not apply to the client’s perspective.
Idempotence can exist as a concept in client-server, but only in the absence of server-managed state. You’d have to return to the more classic examples of idempotent functions, using examples such as those that would appear in a functional programming treatise. The basic case is a function which operates only on its input and neither uses nor manipulates any shared state:
// idempotent because it relies only on its input '5' Request: ---> HTTP GET /math/square/5 Response: <--- 25