Skip to content

Request bodies silently dropped on DELETE and OPTIONS #83

Description

@thomasht86

Summary

content, data, json and files are silently ignored for every method other than POST/PUT/PATCH. client.delete(url, json={...}) sends an empty body with no error, no warning, and no Content-Type header.

Cause

src/lib.rs:468 (and src/lib.rs:641 in _stream):

let is_post_put_patch = matches!(method, Method::POST | Method::PUT | Method::PATCH);
// ...
if is_post_put_patch {
    // content / data / json / files applied only here
}

Reproduction

Against a local server echoing the request line, Content-* headers and body:

httpr   DELETE  json={"id":1}   -> DELETE /x   []   body=b''                        ❌
httpr   DELETE  content=b"..."  -> DELETE /x   []   body=b''                        ❌
httpr   OPTIONS json={"id":1}   -> OPTIONS /x  []   body=b''                        ❌
httpr   POST    json={"id":1}   -> POST /x     [content-type: application/json, content-length: 8]  body=b'{"id":1}'   (control, OK)

httpx   DELETE  json={"id":1}   -> DELETE /x   [Content-Length: 8, Content-Type: application/json]  body=b'{"id":1}'
httpx   DELETE  content=b"..."  -> DELETE /x   [Content-Length: 9]                   body=b'raw-bytes'
httpx   OPTIONS json={"id":1}   -> OPTIONS /x  [Content-Length: 8, Content-Type: application/json]  body=b'{"id":1}'

Impact

RFC 9110 permits a body on any method, and real APIs rely on it — Elasticsearch's DELETE _search / _delete_by_query, and many REST APIs that take a filter document on DELETE. Because the body is dropped silently, a DELETE meant to remove a filtered subset can reach the server as an unfiltered request.

Expected

Bodies are sent for any method the caller supplies one for, as in httpx and requests.

Proposed fix

Remove the is_post_put_patch guard in both request() and _stream() and apply the body unconditionally. (If you'd rather stay conservative, raising on GET/HEAD with a body would at least be loud — but matching httpx by just sending it is the drop-in-compatible choice.)

Suggested tests

  • DELETE with json= sends the JSON body and Content-Type: application/json.
  • DELETE with content= sends the raw bytes.
  • OPTIONS with json= sends the body.

Size

~30 minutes including tests.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrustPull requests that update rust code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions