HTTP requests and responses — Examples

Example 1 — A retry that creates a second order

A client reports that a create sometimes arrives twice. The exchange below is the one that preceded it. Say which instruction is dangerous, why the method made this retry unsafe, and what to change.

request
  POST /orders
  Host: shop.example.test
  Content-Type: application/json
  Content-Length: 84
response
  301
  Location: <the same host and path, secure scheme>
  Cache-Control: max-age=31536000

Solution

cause:  301 is cached and remembered, so every later request is repeated against
        the secure scheme; POST is not safe to repeat and a retry may create a duplicate
change: use 302 for a condition that may change; keep 301 for a permanent move only
        mark the creating endpoint's responses not cacheable
keep:   GET is still retried freely; PUT /orders/42 is safe to repeat

A permanent redirect is remembered long after the condition that justified it, and pairing it with a method that is not safe to repeat turns every retry into a possible duplicate. A temporary redirect, plus cache instructions that match the endpoint’s meaning, leaves the decision with the client. The usual mistake is to treat the redirect as harmless plumbing: it is the client’s standing instruction for all future requests.

Example 2 — A forwarding header the caller chose

A request reaches the application carrying an origin header that the caller wrote. The application now trusts it. Rewrite the handling so the closest component sets the header and untrusted input cannot.

as seen by the application
  GET /profile
  Host: app.internal.example.test
  X-Forwarded-For: 198.51.100.7, 10.0.0.5

Solution

at the proxy: overwrite X-Forwarded-For with the address it observed, never append
              untrusted input, and strip the header on requests arriving from outside
at the app:   read the value only from the trusted proxy, otherwise use the peer
              address of the connection itself
also:         an authenticated profile response is marked Cache-Control: private

Headers carry the context a body cannot, but the component nearest the client must set them and the application must take them only from that component; otherwise any caller can claim any origin. The same review applies to caching, where an authenticated response marked private is not stored by a shared cache. The usual mistake is to trust the header because it is present and its value looks plausible.