deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

nginx limit_except blocks HTTP QUERY with a bare 403, tests of RFC 10008 show

A dev.to test of RFC 10008's QUERY method found curl, FastAPI, Caddy and Traefik pass it through, while Django views and nginx limit_except configs reject it.

nginx limit_except blocks HTTP QUERY with a bare 403, tests of RFC 10008 show

A hands-on experiment documented on dev.to has mapped how the new HTTP QUERY method fares across common web infrastructure, and the headline finding is an nginx pitfall: the widely copied limit_except hardening block rejects QUERY requests with a bare 403 before they ever reach the backend.

QUERY is defined in RFC 10008, which the post says reached Proposed Standard status in June. The method is safe and idempotent like GET but carries a request body like POST, making it a candidate for replacing the familiar search-endpoint-over-POST pattern, where GET cannot express a complex filter object and query strings run into length limits. The RFC itself warns that older proxies, frameworks and load-balancer configurations may not recognise the method yet, but says nothing about what "not recognising" looks like in practice, which is the question the author set out to answer by renting a server and measuring it.

The setup

According to the dev.to write-up, the test ran on a single DigitalOcean droplet in Frankfurt on Ubuntu 24.04, with a FastAPI backend fronted by nginx, Caddy and Traefik on separate ports, plus a standalone Django project using class-based views because Django dispatches methods differently from Starlette. Versions tested: curl 8.5.0, FastAPI 0.141.1 on Starlette 1.6.0, Django 6.1.1, nginx 1.24.0, Caddy 2.11.4 and Traefik 3.7.10. The droplet was destroyed once testing finished.

Client and framework behaviour

curl needed no special handling: a QUERY request with a JSON body, verified against a raw netcat listener, produced a correctly formed request on the wire with no flags or workarounds.

FastAPI answered 200 on a route explicitly declared to accept QUERY, echoing the body back, and a GET-only route returned a correct 405 with an Allow: GET, HEAD header. The caveat is granularity: there is no global switch, so every route that should answer QUERY has to list the method individually.

Django was where things quietly broke. Its class-based views dispatch to a handler named after the lowercased method, so defining a query() method looks sufficient, but the request still came back 405 with an empty body and an Allow header listing only OPTIONS. The cause is that View.http_method_names is a hardcoded list that does not include query, and Django checks it before it ever looks at the class. Extending the list with a single line, http_method_names = View.http_method_names + ['query'], made the same handler return 200 with the body intact.

The nginx trap

nginx itself was not the problem. With a plain proxy_pass and nothing restricting methods, QUERY requests passed through and returned 200, including under ten concurrent requests, and the access log recorded them without any configuration change.

The breakage came from limit_except, a snippet commonly pasted from nginx hardening guides. A configuration restricting a location to GET, POST and HEAD with deny all returned an unexplained 403 Forbidden for QUERY, and the request never reached the backend. As the post points out, this is not nginx mishandling the method by default; it is a hardening pattern written years before the verb existed, when those three methods covered everything a location was expected to serve. Anyone deploying QUERY behind nginx should audit every limit_except directive in their configuration.

Caddy and Traefik pass it through

Both of the newer proxies in the test handled QUERY with their default reverse-proxy setups, no changes and no allowlist to trip over: Caddy 2.11.4 returned 200 with its Via header, and Traefik 3.7.10 did the same with a default file-provider router. The author notes this fits the RFC's warning about older tooling better than anticipated.

Why it matters

The failures here are quiet in the worst way: a Django 405 looks like a routing typo, and an nginx 403 looks like a permissions problem, with no error message pointing at method support as the real cause. Teams tempted by QUERY, especially wherever a POST-based search or filter endpoint currently exists, need to audit every layer between client and handler rather than trusting the application framework alone. The checklist from this test is concrete: extend Django's http_method_names, list QUERY explicitly on FastAPI routes, and grep nginx configs for limit_except, while Caddy and Traefik need nothing at all. The RFC's vague warning about older tooling turns out, in this sample, to mean two specific and fixable rejections rather than a general blocker.

  • #nginx
  • #http
  • #django
  • #fastapi
  • #web-servers