ietf-wg-httpapi / idempotency

Repository for "The Idempotency-Key HTTP Header Field"
Other
15 stars 9 forks source link

Reconsider handling of DELETE (and possibly PUT)? #44

Open jmileham opened 3 months ago

jmileham commented 3 months ago

The spec understandably defers to the HTTP protocol definition of idempotency. DELETE and PUT are inherently idempotent by the computer science definition - application of a DELETE or PUT operation more than once will not change the side effects.

But I'd like to make a case that, at least for DELETE, there is an alternative useful definition of idempotency closer* to the mathematical definition: that in order for a function to be considered idempotent it should produce the same result (i.e. response), not just yield the same side effects. And that's where DELETE will differ unless specially handled. A typical response to a successful delete, absent special handling will be 2xx, whereas subsequent reattempts will likely yield 404. The information "this request you made to delete the resource was actually successful" vs "this resource may have never existed or been deleted by somebody else" is useful to the client's understanding of the distributed system state. So for that reason I'd like to consider sweeping DELETE into the spec. It might create some thorny definitional issues given the naming of the headers and settled understanding of HTTP semantics, but it feels like a solid engineering decision nonetheless.

Then I'll make a less obvious but I'd argue still valid pitch for why PUT makes sense to include. Unlike DELETE, multiple PUTs will likely yield the same result. But once we consider what this service is providing overall for mutative actions, it is providing an at-most-once filter on actions at a semantic client intent level. This has value as a way of denoising "what did the client do and when" from an audit perspective. Being able to say "here is the timeline of discrete client intent and when it was applied across all mutative actions" has value in and of itself, rather than seeing some actions have apparent intent duplications which were in fact just clients unaware of a prior attempts success.

So that's it, that's the pitch. Curious if this feels like a bridge too far for a spec from an HTTP working group in reference to the settled definition of idempotency in HTTP. :) Thanks for listening.

* I say "closer" because I also acknowledge that in #43 I've advocated for returning a fresh response as an alternative to caching the initial response, which violates the classical idempotency definition in a different way. But I console myself that no pure definition of idempotency could even apply for a function applied to a system in two different initial states, so returning stale content isn't inherently more correct either.

awwright commented 3 months ago

I think the type of idempotency you're talking about is more similar to conditional requests. You should be able to use conditional requests with any method, including DELETE, PATCH, and POST, and the If-Match header should accomplish what you're looking for in the idempotent methods.

The reason why you'd want to use Idempotency-Key may be when If-Match would be too restrictive; you may need to retry the operation, but you don't care if someone else made a subsequent request in between.

But with respect to DELETE in particular, I don't see any conflict with "mathematical idempotency". The server is providing exactly that type of idempotency; it's also providing additional information about what change occurred, if any. Clients are free to ignore that additional information (if the resource already existed or not).

jayadeba commented 3 months ago

@awwright "If-Match" is not an option for POST requests (the resource was never created. unless you are using POST for doing updates like Stripe's APIs). The "Idempotency-Key" header focuses on POST requests, and to a certain extent PATCH requests(in case of PATCH, using "If-Match" is possible, but it's an alternative solution). So, in this document, our thoughts are not to discuss alternative options, but strictly focus on "Idempotency-Key" header.

jmileham commented 3 months ago

To add to @jayadeba's remarks on why If-Match wouldn't be a great fit for the use case, the Idempotency-Key protocol does not need to retrieve any prior state (e.g. an ETag) from the server to send a valid request.

The use case for If-Match is similar to but different from Idempotency-Key, as you also noted in your original post: it's more restrictive about applying the change to a different initial resource state, even if the request hasn't been applied yet. My team at Betterment refers these different use cases as optimistic locking and idempotency respectively. Our takeaway after exploring both capabilities quite a bit was that idempotency is something we want all the time in mutative requests and is possible to manage without resource-server-generated tokens, while optimistic locking is only desirable sometimes and requires client awareness of the state it has previously seen.

jmileham commented 3 months ago

Re the specific observation about additional information:

But with respect to DELETE in particular, I don't see any conflict with "mathematical idempotency". The server is providing exactly that type of idempotency; it's also providing additional information about what change occurred, if any. Clients are free to ignore that additional information (if the resource already existed or not).

I disagree with the framing that there is inherently more information. Consider the scenario where the first request to delete returns 2xx (unseen by the client due to network partition) and the second request returns 404. It's different information, and I would argue less client-interpretable information. From the client's perspective here are some alternative realities could have also been true leading to that observed outcome:

  1. A client bug resulted in attempting to delete a resource that never existed.
  2. Somebody else deleted the resource before our first successful attempt. This might mean, e.g., that the deletion is recorded as attributed to another user, which the client would have no way of knowing, and might be a significant detail in our business domain.
  3. It might mean that our authorization to see the resource was revoked and it's still there. (Many modern systems respond to unauthorized resource access with not found to avoid information leakage.)

Receiving 2xx on retry would clarify the client's understanding of what happened with regard its request. In that world, receiving a 404 would straightforwardly mean your delete didn't succeed but the resource is gone, was never there, or you can't see it. With my proposed change, you'd always eventually receive a 2xx after a successful delete, which requires no domain-specific/potentially leaky inference to consider successful and stop retrying.

awwright commented 3 months ago

@awwright "If-Match" is not an option for POST requests (the resource was never created. unless you are using POST for doing updates like Stripe's APIs).

You actually can use If-Match for any method, including POST. It's a little bit clunky—it's clearly optimized for incremental GET, and avoiding lost PUT, so the current capabilities seem insufficient to solve the problem; but it appears that the PUT and DELETE requests we're discussing involve conditions on the server state, whereas Idempotency-Key is solving a smaller set of problems.

awwright commented 3 months ago

Our takeaway after exploring both capabilities quite a bit was that idempotency is something we want all the time in mutative requests

From the client's perspective here are some alternative realities could have also been true leading to that observed outcome:

I think this identifies an entirely different deficiency in HTTP: the ability to retrieve or replay responses that may have been lost after the request has been received and processed by the server.

Idempotency-Key solving this problem: Don't execute this request a second time, if received a second time. While these faults are often indistinguishable to the client (and this suggests there should be a solution that can solve both), this isn't necessarily the case. There's multiple ways to recover from a communication failure like this.

After a network error, the client still knows what state it needs to bring the server to, so it re-issues a GET or HEAD request, and then re-tries unsafe/mutating requests to bring the server to the desired state. Now with this strategy, sure, the response code doesn't matter. e.g. 404/410/200 are all sufficient to know the resource you want deleted is actually deleted. This strategy is an alternative to "the ability to retrieve or replay responses that may have been lost after the request has been received and processed by the server" — it only depends on "Don't execute this request a second time, if received a second time".

And I think if you're interested in following what was each client's intent to do what, that's yet another standardized interface that has to be worked out, I'd need more information. Once you have two clients trying to act on the server state, that each have different goals, I'm not sure any standardized mechanism will help you.

jayadeba commented 3 months ago

@awwright curious: idempotency-key is meant to solve unsafe POST operations, particularly POST create operations (For post resource creation how do you use if-Match (with regard to what state of the resource as the resource never existed prior to this POST)?

For PATCH, you can use if-Match but that's an alternative solution to idempotency-key and the specification is NOT meant to describe all alternative solutions

awwright commented 3 months ago

@jayadeba One example of conditional POST requests would be using POST on a collection with atompub-like semantics (where POST creates a resource at a location chosen by the server). If a GET to the resource lists the resources in the collection, then the ETag would change each time a resource is added. Therefore, you could expect to use POST If-Match: "etag" to insert into the collection only if nobody else has inserted. Or more specifically, to only insert into the collection if it's in a known state, which is desirable if you're inserting to a ledger, for example.

Now with many of these types of operations, where you use POST to modify the same resource, PATCH may be superior, e.g. if you're just appending a line to a log, or modifying a value. But PATCH is limited by media type semantics (certain media types are expected to have certain outcomes, whereas the same document sent over POST is completely server-defined and arbitrary), also it's less surprising when POST has lots of side effects, PATCH isn't typically expected to create resources or have side-effects on multiple resources (though it can, many other resources index the document you're patching).

I think this illustrates the few different types of problems we're dealing with here: ① conflict/lost update resolution (don't overwrite someone else's changes, or don't make a change to a server in an unknown state); ② resumable requests (recover from a request that didn't completely reach the server); and ③ resumable/replay-able responses (get the result of a previous operation that may have been lost in the network). Conditional requests solves (1). Idempotency-Key is part of a solution for (2). And (3) has no automated solution, currently—you have to probe the server state to see if it got to were you want it to be—arguably this is an inherent limitation if you want to keep HTTP's stateless constraint.

jmileham commented 3 months ago

It seems that we agree that the lost updates/optimistic locking problem is not what the idempotency spec is intended to solve. I haven’t seen it said that it should be.

@awwright you suggested that the ability to retrieve or replay responses is something different than what the spec offers, but actually it’s a core facility of the spec for the methods it covers. If you implement it according to the recommendations, you will have a system that replays responses. This is how Stripe’s implementation works, for example - it caches responses and replays them to the client.

You went on to propose a business logic layer solution to reconciling what happened from the client’s view in the scenario I discussed in my post around DELETEs. I’d posit that that app layer reconciliation is a deeply undesirable alternative when you can have an idempotency framework that both provides safety against double submission AND provides replay facilities. Then all you need as a client is an auto-retrying job queue wrapping the requests (or a human clicking the button again until it works) to arrive at eventual distributed system consistency. Any residual errors (e.g. what if the user’s authorization was revoked and the delete request starts persistently 404ing?) will need special handling, but that’s just the breaks of writing distributed software and we can’t provide leverage on that problem from this place in the value chain.

So to summarize based on the numbered list at the end of your most recent post, 1 is out of scope, 2 and 3 are actually covered by the spec for the covered HTTP methods. I would like them to be covered for all mutative HTTP methods because simply having PUT and DELETE behave like the other methods do with regard to idempotency would add significant value at ~no implementation or cognitive cost. The only reason I can see that the spec doesn’t offer them is because it feels in conflict with the way idempotency is defined in the HTTP spec, and it appeared that it wasn’t necessary. I hope I’ve illustrated above why that’s not true.

awwright commented 3 months ago

@jmileham Ok, so then I would suggest calling this "replay-ability" (or resumable responses) instead of "idempotency" which describes the server state. I understand HTTP idempotency as the same concept as mathematical idempotence. When developing APIs it may make intuitive sense to think about the PUT response as the "return value of the function" but this is backwards from the high-level application perspective, where the client is directing the server to apply a function to its own state to derive a new state, e.g. PUT is a function that maps a old server state to a new server state.

Caching may be the best way to think about this, as you mentioned it. Many of the features you're describing are supported by HTTP Caching (POST is cachable with a Content-Location response header and other special considerations, and presumably you could respond with Vary: Idempotency-Key). This could be an avenue to pursue.

However in my personal opinion, anything to do with replaying content should be a separate feature, even with POST. If my request gets sent a second time, I want some way for the server to reply 412 (Precondition Failed) rather than pretending this is the first time it saw my request.

Further, the ability to view effects, responses, or results of earlier requests is a useful feature by itself, and shouldn't be limited to cases where you need idempotency. It's also kind of complicated, and I don't think can be reduced into Idempotency-Key. How do consistency and concurrency issues play out? What if my first request went through but the response was lost, then deleted by another user before my retry? I don't think the current specification is coherent in this regard, as it provides zero guarantees on what the server behavior will be. It doesn't guarantee what the response will be even if nobody else touched the server.

I’d posit that that app layer reconciliation is a deeply undesirable alternative when you can have an idempotency framework that both provides safety against double submission AND provides replay facilities.

The difficulty is that HTTP is fundamentally a stateless protocol for exchanging states, so state synchronization is going to look different for each server and client.

jmileham commented 3 months ago

While I used the mathematical definition of idempotency to illustrate that a consistent response is a valuable property, and I don't cede the point that it applies here, the heart of the case I'm making in this github issue is just pragmatic engineering. If the spec were expanded to include DELETE and PUT we would have a powerful primitive for building distributed system consistency across all mutative methods: semantic at-most-once-execution that can be paired with reliable client-side retry to achieve exactly-once execution semantics over HTTP (assuming the request is valid and applicable). If we forego my proposed change, we have a less powerful tool. A client can't tell reliably whether a DELETE worked without app layer reconciliation, even without additional interleaved server state change. A server can't ensure that PUTs are logged as being semantically requested only once. That's all that I'm discussing here.

Whether the protocol described by the draft supports replay/caching or not (and whether it should) is not actually relevant to this github issue, though as drafted the spec clearly does. If you want to make a case against response caching and replay, you'd have to take it up with the authors of the spec, presumably in a new issue.

jayadeba commented 3 months ago

@awwright for both the examples you have, it's very unusual to associate an etag to a resource collection and very inefficient- as a client you need to download all the resources in a resource collection, for every new addition to the collection. In many cases resource collection result set run into millions of records- even for images (CDN use cases) such an implementation is inefficient. Your second example, you are always appending to a ledge, never updating it, creating a new ledge entry is like creating a new resource, you don't need to know the previous state of the ledger, but you need to however ensure that you are not adding a duplicate entry (for which you can use the idempotency-key header). In summary, a new resource creation exactly once semantics is what this spec addresses where you never had any previous resource state. With PUT, PATCH, DELETE you always have the previous resource state so. Etag is a nicer solution, while someone may choose to use "idempotency-Key" for a PATCH requests in absence of Etags.

jmileham commented 3 months ago

@awwright for both the examples you have, it's very unusual to associate an etag to a resource collection and very inefficient- as a client you need to download all the resources in a resource collection, for every new addition to the collection.

@jayadeba do you also agree that efficiencies aside (which could likely be addressed reasonably with a space/time tradeoff optimization), lost update resistance / optimistic locking is not the desired behavior of the idempotency system being specified here? Here's a concrete use case to illustate: A single customer making multiple discrete deposits to an account (say from different source accounts) that arrive out of order (e.g. due to network instability and retry timings) is a valid user story that still benefits from idempotency. To put these properties in user story form:

I believe that discussion of ETag based solutions are out of scope for the spec because they solve a fundamentally different problem. If that wants to be considered, I don't think this is really the github issue for it?

jayadeba commented 3 months ago

@jmileham I would agree with everything you said above (the exact 2 use cases you have cited above is what this spec solves). lost update/optimistic locking and etag based implementation for the same is totally a separate issue, and they have nothing to with request idempotency. And it's out of scope for this specification. That said, thank you both for a good/in-depth discussion on this. I'm going to resolve and close this issue.

jmileham commented 3 months ago

Hi, sorry, my read of the issue I opened is that it was somewhat threadjacked by the optimistic locking etag discussion. I have not heard any discussion of the proposal I put on the table. Should I not expect one?

jmileham commented 3 months ago

Since it's probably somewhat lost in the fray, I'm going to jot down user stories for the actual proposal at the top for clarity on what I'm proposing in this github issue and why:

To reiterate from my original post, I understand that both of these changes would represent substantial opinion shift for the spec to take, but the results have been highly valuable in our use at Betterment, and I hope that the spec authors will consider adopting them.

awwright commented 3 months ago

@jayadeba

it's very unusual to associate an etag to a resource collection and very inefficient- as a client you need to download all the resources in a resource collection, for every new addition to the collection.

You shouldn't need to download any of the resources in the collection, you can just make a HEAD request to the collection to get its ETag. Though I'm not trying to suggest this is a suitable solution by itself.

awwright commented 3 months ago

@jmileham I'm just still unsure how what you're asking is different from other HTTP features, it seems the features you're talking about are already available, or should be pursued in other areas.

semantic at-most-once-execution that can be paired with reliable client-side retry to achieve exactly-once execution semantics over HTTP

This sounds like conditional requests, as I initially suggested. In particular, conditioning the request on not having seen the transaction/request ID before, which is definitely useful in many situations, but not necessarily all of them.

I want to see a population of singly applied updates at the semantic customer intent level to demonstrate that customer intention is applied exactly once, and to not see no-operation retries brought on by network partitions on the return trip as spurious updates that need further scrutiny.

User intention is difficult because HTTP is necessarily abstracting away many aspects of the server to the client, and vice-versa. HTTP does not capture intent, or at least not completely. I would say the major distinction between HTTP and RPC is that HTTP allows for a wider variety of intentions to be seen through, even ones not predicted or specifically supported by the server ("loose coupling").

HTTP can, though, capture conditions which partially communicate intention (when expressed with fields such as If-Match or Idempotency-Key). Though this isn't reliable. If the user is not encoding a condition in their request, it's probably because it's not that important. If it was important, then you'd want to encode that as a condition... "Since the room is dark, turn on the lights" becomes "If the room is dark, turn on the lights."

...However this is just as good: "I just want it to be bright, turn on the lights and if they're already on fine, whatever."

To the user these are the same request, and trying to interpret these as different (as the server) would be a mistake. If this genuinely is important to the server, for some reason, then you need even more detail than this. Can you elaborate on what that might be? (And I can imagine cases where this is genuinely meaningful, but it's in cases like multi-authority or decentralized databases where the operations have to be commutative because different authorities might apply the operations out-of-order.)

when clicking on a button that results in an HTTP DELETE operation, I want to receive the server-developer-intended terminal result of my deletion operation even if a network partition causes my first submission to fail and I have to submit again, so that my user experience is not broken, and there is no need for leaky inference to consider the deletion successful

I see two things being confused here. This is why I keep alternating between saying "this sounds like conditional requests" and "this sounds like caching/replayed responses."

If you submit a DELETE request with no conditions on the current server state—i.e. you just want it gone under all circumstances—then you can safely retry the DELETE request. You didn't care what was there during the first try, there's no reason you would care what's there the second DELETE. Either you deleted it, or a previous operation with a lost response deleted it, or someone else deleted it, you just have to keep retrying DELETE until it goes through (until the user evaluates the new server state and says stop, that's enough).

Now if who or how it was deleted is of consequence, then that's the job for some sort of revision control system, in which case the first person to get the request in will "win" (or maybe you journal all attempts), but even in this case who "won" is completely arbitrary. If it's not arbitrary for some reason, if who got the operation in first is important, then you have to elaborate on why, and convey that information to the server.

The reason you're asking for the additional feature is presumably because you want a condition on the DELETE. There is some situation where you don't want the DELETE to go through, and you want to convey this intent to the server.

jmileham commented 3 months ago

I've laid out the semantic and use case differences between http conditional requests and an idempotency key solution quite clearly. I believe that @jayadeba has also confirmed for you that they are not the same and discussing conditional requests is not in scope for the spec in general or this GitHub issue in particular. While I appreciate your energy on the topic I will not engage further on the topic of conditional requests in this thread.

awwright commented 3 months ago

@jmileham I noted you listed some use cases, but I asked for some elaboration, as the concrete examples I came up with around your use cases don't seem to benefit from the Idempotency-Key header, e.g. in my "just turn on the lights" example.

I'm trying to discern more details about your intended use here, as these do have implications in areas like data consistency. The specifics of the use cases will suggest different courses of action about how the server ought to respond with. I offered multi-authority/decentralized databases as an example: this is an instance where the client must accurately convey its intent in each operation—as the same request may have different effects on different nodes in the network.

Now @jayadeba seems to share my assessment that ETag is not a sufficient replacement for Idempotency-Key. My point is that Idempotency-Key is somewhere in the class of conditional requests and/or conflict resolution features (indeed, the specification refers to the 409 Conflict status code).

jmileham commented 3 months ago

So I assume we agree that "just turn on the lights" isn't an example of DELETE, but it could be an example of PUT. But the valuable property of PUT that I refer to in the user story above is the service not logging more than one successful attempt if the client never presented the request with more than one unique idempotency key.

There is no existing feature of conditional requests meeting the requirements laid out in my user stories. Extending the idempotency spec to cover PUT and DELETE would satisfy the requirements.

In order to have a discussion reaching a different conclusion than support of the proposal (other than rejecting the idempotency frame altogether and suggesting that something else entirely should be created, which I suggested above would be a good use case for a new github issue of your own), it seems to me that you'd need to present one of the following:

  1. Evidence that my proposal doesn't offer the guarantees I claim (I've not heard you say this)
  2. Evidence that existing HTTP functionality is equivalent or preferable (You've offered alternatives that exist but are not equivalent or preferable, or alternatives that do not exist that might be equivalent were they specified)
  3. Evidence that concludes that the user stories I offered are not valuable.

I'd be interested in a conversation going all the way down any of these three paths. We don't need to get into decentralized database theory to falsify the simple claims I'm making above.

jmileham commented 3 months ago

Also, re this supposition of confusion:

I see two things being confused here. This is why I keep alternating between saying "this sounds like conditional requests" and "this sounds like caching/replayed responses."

If you are only able to understand the idempotency spec's behavior in terms of a confused composition of other HTTP features that as specified don't offer the desired behavior, I'm not sure we can begin a discussion in earnest.

jmileham commented 3 months ago

Just in the interest of advancing the conversation I'd like to have here, because we seem to keep getting hung up on the startup, I think there may exist arguable reasons not to want to extend the spec to cover DELETE and PUT (taking the other side of my own proposal) that don't rest on not-yet-existant HTTP features elsewhere. I'll try to make a defensible counterargument to my proposal here:

[counterargument] A reason to keep the spec as-is would be that we decided that the mission of the idempotency spec must strictly be limited to safety against applying an operation more than once.

Now, back to my regularly scheduled advocacy for my proposal:

I take the pragmatic engineering-centric view that by not covering all mutative methods, the spec kind of outsmarted itself. It leaves significant (free) value on the table. From a high level, with my proposal, clients can be less "smart" and reach a more mutually understood, fail safe, successful outcome over unreliable networks with fewer successful network round trips.

Without DELETE covered, clients can't be confident that their delete was successful. In financial services - my company's domain - if the client lost authorization to see a resource (an example I described above) but interpreted the 404 it received in response to a delete as success, that would be a grave bug. Some part of the distributed system would believe an object was deleted when it was not. Having a protocol in which clients that can expect (eventually, after enough retries) a 2xx in response to any successful operation is hugely valuable. The can then treat any other response as potentially unsuccessful, which will then need reconciliation (manually or via automated domain-aware reconciliation software), but is much better than making a false inference.

So there is an important meta safety offered by covering DELETE. Operations that eventually respond with 2xx are the only ones that can safely be considered successful by the client.

The PUT argument is not one of safety, it's just a denoising function. It's a nice to have. A little treat that you get for free for not special casing the covered HTTP methods so hard. It's this: Sometimes a successful PUT will need to be retried by a client in order for the client to successfully receive a response, so it can be sure it worked. But if the PUT is delivered with a consistent idempotency key, the server can know that it was only one intent. We can skip the business logic and duplicative write, and more importantly we can skip journaling that the PUT was attempted again. We get server side awareness of each discrete successful client PUT intent, not just the full set of PUTs that hit us. It's nice. Don't we deserve a little bit of a semantic deduplication freebie? I think so. :)

awwright commented 3 months ago

So I assume we agree that "just turn on the lights" isn't an example of DELETE, but it could be an example of PUT.

Correct.

But the valuable property of PUT that I refer to in the user story above is the service not logging more than one successful attempt if the client never presented the request with more than one unique idempotency key.

This carries the implicit assumption that all clients will be sending this header, which is only possible if you're developing a single party application (where you own and manage both the server and the client). But in that case, you just make that the design of the application, no standard is needed. The purpose of spinning this into a specification is to coordinate between multiple parties. In general, the server can't expect the client to send an Idempotency-Key header in its requests (PUT or otherwise), the client can't expect the server will honor the field, and the client doesn't benefit from sending it since the request is already idempotent.

And two clients with exactly the same motivations or intentions may send the request in all sorts of different ways. You might see a PUT request setting brightness=100% with If-Match: "etag-from-when-lights-were-off", or Idempotency-Key: "user A action at 2000-12-01", or some other header that specifies "only if brightness<100%", or all or none of these headers; they all do the same thing (in the given situation), and thus all potentially convey the same intent. Not to say your use case is impossible, but you can't rely on Idempotency-Key as a user intent tracker when multiple parties are involved. The purpose of having a standard is to coordinate behavior, therefore I don't think this use case is in the scope of the specification.

jmileham commented 3 months ago

I can see the POV that you can't universally get the deduplicating behavior from PUT across all clients and servers just because you ship a web standard. But all HTTP API contracts rely on standards, and there's no reason that a particular contract couldn't require Idempotency-Key to be used on all mutative operations. Promoting availability of clients and servers that implement the standard "off the rack" is another purpose of standards. Defining the semantics of that standard such that it doesn't unnecessarily rule out a capability in a coordinating client/server pair seems like a win to me.

In the modern consumer web, virtually all scaled web applications employ a JS-backed thick client. So the marginal value of supporting PUT in this way would accrue to those apps as soon as they adopted the standard.

Even more forward looking, in a world where web browsers universally adopted Idempotency-Key for mutative requests by default, you could imagine the world getting this benefit (and yes, I do believe the web would be better for it if all clients and servers supported this spec for all mutative requests by default, and classic web forms are a great fit). There'd be some subtle UX behavior to define (Are there modals that say "this submission may have already been applied - do you want to resubmit?" When the server returns an ambiguous response?) that might merit yet another standard layered on top of this one, but could be 100% compatible with the final version of this spec if we didn't foreclose on that path.

Anyway to recap, one purpose of standards is that they can become ubiquitous, broadly supported everywhere, creating value that wasn't available when the standard was released, so I'd encourage the standard not to foreclose on that possibility because it was circumscribed too narrowly. A second purpose is simply to specify a protocol that can have value for opt-in participants immediately. A third is to encourage an ecosystem of compatible clients and servers, which can create a glidepath to the first purpose.

Take Stripe again as an example. They ship their own SDK client alongside their service layer even absent a standard (though I believe their version of idempotency doesn't support put or delete - I am acknowledging the value of their approach even though I think there's more value to unlock). A great outcome of this spec writ large would be for another firm to adopt the same pattern with off the rack tools (either via a B2B SDK or in their consumer app) and iteratively build a safer, more semantic web.

jmileham commented 3 months ago

and rhetorical feedback:

This carries the implicit assumption that all clients will be sending this header ...

I did not make the assumption you insinuate, as you can see from my response (though the discussion is valuable! I actually appreciated the point you made, because it clarifies the conversation). But I believe I've earned enough credibility in my contributions to this conversation so far to make it inappropriate for you to impute that I'm making facile mistakes of reasoning. I'd encourage you to show up with more curiosity about why somebody might believe something is of value even if you do not (yet?). There's more for all of us to learn through the conversation.

jmileham commented 3 months ago

(And even if I didn't have particularly fleshed-out views, the IETF will miss out on a lot of great contributions if the default assumption is that anybody entering a conversation is mistaken, rather than engaging with curiosity about what they might add to the conversation.)

richsalz commented 3 months ago

Let's take it down a notch please and be respectful of each other.

asbjornu commented 3 months ago

What is making me a bit confused about this proposal as well as much of the spec itself and Stripe's implementation is that the seemingly most desirable effect of using Idempotency-Key is not to make any transactional guarantees about idempotency, but to replay previous responses to the same idempotency key.

While I see the value of caching and replaying previous responses, I think it's an orthogonal feature to Idempotency-Key – or at least what I would like Idempotency-Key to represent. If all Idempotency-Key could give me was at-most-once-processing of requests and replay of responses, I would prefer to avoid the spec altogether and implement a bespoke solution for idempotent requests that at least attempt to achieve exactly-once-processing of requests. With the user stories outlined in https://github.com/ietf-wg-httpapi/idempotency/issues/43#issuecomment-2048511678, I believe the spec can explain how to achieve several levels of protection, but we still need to reach consensus on that and write the actual text in the spec.

So while I'm not against the proposal to include DELETE and PUT in the spec, I'm a bit 🤷🏼 about the reason to include them. Replaying a response would not be why I would want Idempotency-Key to be applied to other HTTP methods, especially considering the discussion in #41 where the replayed response could be a random, ambiguous error message.

jmileham commented 3 months ago

@asbjornu

What is making me a bit confused about this proposal as well as much of the spec itself and Stripe's implementation is that the seemingly most desirable effect of using Idempotency-Key is not to make any transactional guarantees about idempotency, but to replay previous responses to the same idempotency key.

Yes! Thanks for raising. I'm very interested to understand how the authors and WG view the purpose of the spec, and that gets to the heart of what approach to take to this proposal and would be clarifying about others like #43 as well.

My experience has led me to believe that pairing at-most-once execution of semantic intent with some form of response replay (in my preferred transactional implementation, if the response has a body at all, returning a fresh representation of the resource rather than a cached representation) is a pattern better than the sum of its parts. There is no more efficient way (in terms of number of successful network round trips) to enable client/server agreement about whether a discrete intent was applied and what the result was without unintended duplication. The Idempotency-Key enables server understanding of discrete client intent, and the replay response enables client understanding of that intent's resolution. At least one of these two features is valuable across all mutative HTTP methods, including DELETE and PUT.

asbjornu commented 3 months ago

At least one of these two features is valuable across all mutative HTTP methods, including DELETE and PUT.

@jmileham, yes I can see that. And I'm not against putting it into the spec. What I'm against is doing that and calling it a day. If we do this and establish a pattern for a generic, domain-agnostic idempotency service, I want the spec to make that explicit as a (low) level of protection and also explain how to achieve a higher level of protection with a domain-integrated idempotency implementation.

jmileham commented 3 months ago

@asbjornu I'm not sure I understand the distinction lbetween a domain-agnostic idempotency service and a domain-integrated implementation - is it the same as what I've referred to as non transactional intermediary service vs integrated transactional service? If so, totally agree about wanting to codify the range of spec-compliant implementations (standalone vs integrated). That's my hope for #43. Do you see a direct dependency on that decision for making this one? Because it seems that a standalone idempotency service could offer DELETE and PUT support as described just fine, right? While both questions go to what the spec is for, really, I'm not sure you'd have to pair the decisions?

jmileham commented 3 months ago

(And even though my top post on #43 is poorly worded, I am a big advocate for the integrated implementation as the ideal where feasible for multiple reasons - fresh replay, no cached non-final states)

asbjornu commented 3 months ago

@jmileham:

I'm not sure I understand the distinction lbetween a domain-agnostic idempotency service and a domain-integrated implementation - is it the same as what I've referred to as non transactional intermediary service vs integrated transactional service?

Yes.

Do you see a direct dependency on that decision for making this one?

It's not a direct dependency, no. As long as we agree on what the ideal implementation is, I'm +0 about the change as it's a part of the spec I'm most likely going to ignore in my implementations.

jmileham commented 3 months ago

Cool, makes sense to me. Maybe a helpful way to frame the conversation is there are some "front of house" concerns - what contract do we want to present to clients - like:

And some "back of house" concerns like:

These concerns obviously leak between layers. The standalone choice means unresolvable non-final responses to the client (if you preserve at-most-once guarantee; which I believe is crucial to the spirit of the spec). The potential replay response strategies yield different client visible results. The fingerprinting and security implementation choices reveal different weaknesses to attackers.

I'm admittedly new here and I don't know whether the front/back of house distinction makes sense to others or is a good way to frame up the conversation, or a per-feature approach or what. Many of the concerns interact, which I think is why it's hard to have discrete conversation threads (which, trying to keep things separate contributed to my earlier feistiness, sorry). But it also makes it hard to keep things straight for folks just joining the convo in mega threads (which I am guilty of posting a lot to).

Unless there's more discussion to have on the topics in the current issues, I'm gonna take a beat and hope for a steer from somebody about what's next. :)