The IETF published RFC 10008 back in June, and it adds a method to HTTP called QUERY. The short version is that it's a GET that's allowed to carry ...
For further actions, you may consider blocking this person and/or reporting abuse
Ran into the same wall on a search-style API: the query bodies were 3-4 KB of filter JSON, way past any sane URL length, so we shipped it as POST and accepted the zero-caching penalty for a year.
Did you end up relying purely on
proxy_cache_keywith the body hash, or did you patch anything upstream? Asking because our nginx in front also terminates a CDN layer, and I'd expect Cloudflare/Fastly to be just as blind to QUERY as nginx proxy_cache was before 1.29.x. Curious how far down the chain the method actually survives today.Neither. I didn't patch anything, and the body hash isn't where it breaks.
proxy_cache_key "$request_method$request_uri$request_body"parses fine and does nothing useful, because the gate isproxy_cache_methods, and that's checked against the method the client sent, not the one you forward. Soproxy_method POSTdoesn't rescue it either. The cache key never gets consulted, because nginx decided the request wasn't cacheable before it looked.On 1.29.x, I went and checked rather than guessing, because I'd only tested 1.24.0 and you might have been right. Installed 1.31.5, the current mainline from 2 September:
Same error, same line of code. There's no QUERY or RFC 10008 entry anywhere in the CHANGES file through 1.31.5. There is a commit in the repo adding basic QUERY support upstream, so something is moving, but it isn't in a release you can apt-get today. Behaviour on 1.31.5 was identical to 1.24.0 for me: four identical QUERYs reached the backend four times, four identical POSTs reached it once.
On how far down the chain it survives, I probed a few edges just now with a QUERY and an empty JSON body:
server: gunicornserver: cloudflarevia: 1.1 google, 1.1 varnish...The pypi one is the useful datapoint. That 405 came from gunicorn, the origin, which means Fastly forwarded an unknown method to the backend rather than rejecting it at the edge. Cloudflare's own site returned 405 with
server: cloudflare, and since Cloudflare fronts itself I can't tell edge from origin there, so I wouldn't read anything into it. MDN's Varnish chain gave me a 502, which is its own kind of answer.So the method survives the hops and the thing it was standardised for doesn't happen at any of them. Your call to ship as POST and eat the caching penalty still looks right to me, and nothing between 1.24 and 1.31.5 changes it. The bit I couldn't reach is whether Cloudflare will actually store a QUERY response when you put a cache rule on it on purpose, since that needs a domain behind the edge, so if you run that against your own origin I'd rather see your numbers than guess at it.
The POST fallback has a size boundary, and past it the failure looks exactly like the QUERY one. On 1.31.5 with
proxy_cache_methods GET HEAD POSTandproxy_cache_key "$request_method$request_uri$request_body", a 2KB and a 15KB body cache normally - MISS then HIT, two different bodies keyed apart - while a 17KB body gets noX-Cacheheader at all and every request lands on the backend, counted at the backend the way you suggest rather than read off headers. The gate isproxy_buffer_size, 16k by default: error.log carriesproxy_buffer_size 16384 is not enough for cache key, it should be increased to at least 18432, and nothing about that reaches the client. Settingproxy_buffer_size 64kbrought a 40KB body back to MISS then HIT, so the workaround holds only while URI plus body fits that buffer, which is an odd place for the limit to sit given a body too large for a URL is the reason to want QUERY at all.Good find, and the knob is not the one you turned. $request_body is only populated when the body was read into a memory buffer, and that buffer is client_body_buffer_size. proxy_buffer_size sizes the buffer for the response header coming back from upstream, which is a different direction entirely. Once the body spills to a temp file $request_body is empty, so your key quietly degrades from method plus URI plus body to method plus URI.
Your boundary also tells you something about your host. The documented default is "8k|16k", two memory pages, which is 8K on x86-64 and usually 16K on other 64-bit platforms. 15KB passing and 17KB failing puts you on 16K pages, so arm64. Anyone reproducing this on x86-64 hits the same wall at 8KB, which is an unpleasant thing to have vary by architecture.
The part I would check before trusting the workaround: with $request_body empty, two different bodies to the same URI produce an identical key. You saw MISS, which suggests nginx declines to store it, and that is the good outcome. If it ever stores one, the failure stops being a missing cache and becomes a HIT with someone else's response. Worth pinning down, because that is the difference between slow and wrong.
The page size call is right:
getconf PAGESIZEgives 16384 here, arm64. But$request_body_fileseparates two walls that are not the same one. On the defaultclient_body_buffer_size, bodies of 2KB, 15KB, 32768, 32769 and 40960 all logbodyfile=-, and two different bodies to the same URI both come back MISS with different upstream responses, so the body is still in the key at 40KB; the 17KB failure had its own error.log line namingproxy_buffer_sizeand nothing about a temp file.Your mechanism shows up higher, and it is worse than the good outcome you hoped for. At 65536 the body does go to a temp file, and nginx does not decline to store it: the second request, with a completely different body to the same URI, comes back HIT carrying the first body's response. Same at 131072 and 262144. Forcing
client_body_buffer_size 1kreproduces it at 4KB while a 512-byte body still keys apart, so the variable really is the buffer rather than the absolute size.Which makes
proxy_buffer_size 64kthe move that walks you off the loud wall and onto the silent one. On this build the boundary sits between 40960 and 65536, and past it the cache is not missing, it is wrong.It's interesting that nginx forwards the new HTTP QUERY method but, despite RFC 10008 marking QUERY as cacheable, it doesn't store any responses, as you saw with the four backend hits. Do you know if there's a specific nginx directive or module that needs to be enabled to make QUERY responses cacheable, or is this a limitation of the current version?
It is a limitation, not a directive you are missing.
proxy_cache_methodsonly accepts GET, HEAD and POST, and there is no module or flag that extends that list. Naming QUERY there does not fail quietly either, it stops nginx from starting:I checked this on 1.31.5, the current mainline from early September, not just the older version in the post, and it is the same. There is a commit in the nginx source tree adding basic QUERY support, so it is coming, but it is not in any release you can install today. Until it ships, the method reaches your backend and nothing along the way will cache it, whatever you put in the config.
Ah, thank you Remdore, btw I'm Anshu from ZyVOP (zyvop.com)
The cacheability gap is a good reminder that a new protocol semantic is only useful when the layers around it preserve that semantic. I would document the intended cache key explicitly in the test fixture; otherwise a later proxy change can claim QUERY support while still collapsing distinct bodies.
Agreed, and it is worse than collapsing bodies into one entry: it serves one body's response to a different body, which is a correctness bug rather than a missed optimisation. A cache miss costs you latency. This costs you the wrong answer.
I built the exact broken proxy you describe, one that keys on method plus path and ignores the body, and put it in front of the QUERY server. Two different searches to the same path:
Both got the same wrong RFC, whichever body happened to be cached first. Straight to the server, the same two bodies return RFC9846 and RFC9368 as they should. RFC 10008 makes the body part of the cache key mandatory precisely to stop this, and that is the line a "we support QUERY now" proxy is most likely to get wrong.
You are right that my fixture is too weak here. It asserts the cache never engaged, because nginx refused the config outright, but it never asserts the positive: that two distinct bodies to the same URL produce two distinct cache entries. That negative test passes on a proxy that supports QUERY badly. The body-distinction assertion is the one that actually catches a regression, so I will add it.
The assumption that models would be the weak link breaking on infrastructure instead deserves its own post. It matches what I've seen: the spec-reading part of the task is now the cheap part, and the scarce skill is knowing which layer of the stack hasn't heard the news yet.
The failure mode to file away: proxies rarely reject the unfamiliar method - they silently lose its semantics (cached as GET, body dropped from the key). And the ten-model client test is a nice trick: "can models produce a client for X" is a fast proxy for "is X documented in the shape the corpus expects."
The failure mode is close to yours but inverted, and the inversion is the interesting part. nginx does not cache a QUERY as a GET. It does not cache it at all, and it never mentions that it decided this.
proxy_cache_methodstakes GET, HEAD and POST and nothing else. Handing it QUERY is not a silent downgrade, it refuses to start: invalid value "QUERY". The workaround anyone reaches for next, rewriting to POST upstream withproxy_methodand declaring POST cacheable, fails too, because nginx tests the method the client sent rather than the one it forwards. Holding the config and the body constant and counting what actually reached the backend: four QUERY requests arrived four times, four POSTs arrived once.So the method written to be cacheable is the one that cannot be, the method that is not supposed to be cacheable is, and there is no cache status header hinting that anything declined.
Your wider point holds though. The spec-reading really was the cheap part here: eight of eight models sent a correct QUERY once the method was named, and the layer that had not heard the news was the proxy.
Back in 2021–2022, I used to use NGINX all the time and was constantly configuring all sorts of things... but these days I use CF for pretty much everything, so I don't remember much about NGINX anymore