Comment by 100ms

1 day ago

Including a strong motivating example might have helped sell this, using an example that could trivially be expressed as a GET is extremely distracting.

Even imagining a QUERY with a large JSON filtering structure, or say an image input as request body, it feels extremely odd to include the request body as part of the cache key. It also implies an unbounded and user-controlled cache key, with the only really meaningful general caching strategy being bitwise compare of the request body (or a hash), which in a hostile scenario implies cache busting would be trivial.

This invokes multiple semantic oddities in one go with obvious difficulties for a very niche use case. If I'm writing a service that needs complex filtering or complex input like an image, any form of caching (e.g. individual data columns of a join, or embeddings keyed by perceptual hashes of a decoded image input) is going to be far away from the HTTP layer and certainly unrelated to the exact bit representation of the request on the wire.

Why even bother trying to capture this in a generic way?

I would be far more inclined to try and capture this caching semantic as a new header for POST. Something like "Vary: request-body" or similar. Perfectly backwards compatible and perfectly ignorable for all but the 0.1% of CDN use cases where the behaviour might turn out useful

> It also implies an unbounded and user-controlled cache key,

The query part of GET's URI is also barely bounded in practice and user-controlled, and is indeed used as part of the cache key (because it's a part of URI), so I am not sure why you raise this objection at all.

  • > and user-controlled

    I've found some sites that tack on a session ID and if you try to tamper with the URL in any way, it sends you back to "Page 1" really annoys me lol at that point let me skip to any page with your web UI.

  • Well, because it is more code. Current caching software caches by headers + query string. It now needs to be expaned to cache by body too.

    It feels very pointless and there is no drawback of just using POST

The browser can simply store a collision resistant hash (e.g. SHA-256) of the body, if it wants a smaller cache key. I can't really think of any caching related attacks that don't equally apply to a query parameter. Generating a unique 30 character query parameter is just as easy as generating a 30 MB request body, if you want to flood the cache.

  • Not necessarily that simple, as you'd have sort all the input parameters to maintain a useable cache key. Not especially difficult, but if the data is large and so re-allocation and sorting is required, then you're starting to open up the attack surface where bugs might have been introduced.

    • I think the only sane approach for caching comparing if the body is identical and not apply any content type specific transformations. It's the safest choice, and the cache not being effective in some edge cases isn't a big deal. The client can always canonicalize the data itself, if it matters for its use-case.

      Even if the spec says that different representations are equivalent, it's quite common for applications to treat them differently. For example field ordering in json objects is supposed to not matter, but some serializers care if a type discriminator is the first field.

    • Do you have to? Is it common to treat ?a=1&b=2 the same as ?b=2&a=1 in browser/CDNs/etc?

      Seems the spec puts this as a MAY. I think I doubt it will be implemented in generic ways, except perhaps for urlencoded payloads. After all you cannot normalize in general without knowing the query language. At the backend it does not matter, may as well cache one level deeper based on the parsed input irrespective of QUERY or not.

      1 reply →

Regarding the body used as a key for the caching: in the RFC, from my understanding, it's indicated that we can use Location as well:

Exemple:

``` QUERY /search HTTP/1.1 Content-Type: application/json

{ "filters": { "region": "asia", "status": "active" }, "sort": "created_at", "limit": 500 } ```

can answer

``` HTTP/1.1 303 See Other Location: /queries/results/f3a9c1d7 ```

And then you can access later `/queries/results/f3a9c1d7` using a pure GET call, and cache this instead

Not all usage scenarios are the public internet, and something doesn't have to be useful on the public internet to be standardized.

Realistically, systems for the public internet will use a secure hash as the cache key so it'll always be the same size. The cache key already includes a URL that can be very long, and an arbitrary set of header values.

  • Except that by definition, in a URL the data has no implicit meaning so for a cache hit you need an exact match, including order and case, but for a list of POST parameters, they could legitimately be in any order and so you can't just hash it all as a blob, you need to sort the keys, possibly copy data around (unless using keys plus hash), probably allocating more memory, etc. I'm pretty certain we'll see at least one CVE out of the first few implementations of this!

    • POST/QUERY data can be in any format. Who are you to say order doesn't matter? Are you sure you can even parse it? Mine is in DES-encrypted (with key "password") base85 DER, you really gonna implement that in your proxy?

      4 replies →

One example - I'm building an MCP server at the moment for a database I'm working on. In ChatGPT I want to do dry-run posts first that roll back before committing - both are POST requests with a property - and it loves to trigger the safety layer in the tools (for various reasons, it's hard to debug exact causes)

But I think this would make it better - QUERY before POST means different request types, not just the same with a safety flag.

> It also implies an unbounded and user-controlled cache key.

While the concern is valid, caching is entirely optional at query level, therefore it is totally valid to cache only certain "filters".

Sure you can provide an image as request body, but you could already do it with b64 query parameter. If you try hard enough, you can poorly use any proposed standard. GET with query parameters already is opaque and makes cache busting trivial.

> Why even bother trying to capture this in a generic way?

I guess it's about resolving the odd semantics of using POST which is not idempotent and thus allowing easier control flow of caches and retrys.

Your perspective is 100% correct if you think at the application-layer, but with a dedicated method, you can have that behaviour out-of-the-box out of your HTTP infrastructure (whether it's at your hyperscaler's router or your apache/nginx/browser whatever) and stop implementing yourself the post-as-a-query edge case.

I would use a hash of the body content (the query) as a URL parameter

/?hash=123456789

  • Why? That's pushing more work to do both on yourself and the cache.

    • Actually this is a use-case supported by this RFC [1]. You accept an arbitrary QUERY /search/ and you cache it on your side (or in a middle box somewhere such as a CDN edge) you can return in your response:

          Location: /search/?queryHash=SOMECDNHASH
      

      The browser can then cache that Location and the next time convert that same QUERY /search/ into GET /search/?queryHash=SOMECDNHASH.

      Sure, it is more work for your webserver to compute that and potentially the browser to cache it's knowledge of that QUERY, but it potentially gives you an advantage in keeping things like CDN edge caches generally aware of client/browser caches in a way that can be performance optimized.

      [1] https://www.rfc-editor.org/info/rfc10008/#section-2.4

If you control the full stack then the functionality described here can be implemented with POST. The only way this comes into play is if some second party client of your service is trying to impose rules on how your backend works. My answer to that is no. I will be defining the contract by which my services operate.