restore_payload drops the body of multipart/form-data and x-www-form-urlencoded PUT/PATCH requests

Open Beginner friendly
#41 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
api

Research direction

Start in rolo/request.py at restore_payload and run the multipart reproduction from the issue for POST, PUT, and PATCH requests. Done means form bodies for non-POST methods are preserved instead of becoming empty, while the existing POST behavior remains intact.

Written by the indexing model from the issue text.

Description

restore_payload drops the body of multipart/form-data and x-www-form-urlencoded PUT/PATCH requests

Summary

restore_payload() only reconstructs a consumed form body when request.method == "POST". For any other method that legitimately carries a form body — notably PUT and PATCH — it returns the already-emptied request.data, so the body is silently lost. This surfaces when rolo is used to proxy requests (e.g. the API Gateway HTTP API AWS_PROXY integration in LocalStack): a multipart/form-data PUT/PATCH reaches the target with an empty body while Content-Length is preserved.

The HTTP spec places no restriction tying multipart/form-data (or any media type) to POST — RFC 9110 lets PUT/PATCH carry any representation, and clients routinely send multipart via fetch(url, {method: 'PUT', body: formData}). The POST-only assumption here appears to trace back to HTML <form> (which only supports GET/POST), not to HTTP itself.

Affected code

rolo/request.pyrestore_payload (rolo 0.8.3):

def restore_payload(request: Request) -> bytes:
    if request.shallow:
        return b""
    data = request.data
    if request.method != "POST":          # <-- drops PUT/PATCH form bodies
        return data
    if request.mimetype == "multipart/form-data":
        boundary = request.content_type.split("=")[1]
        fields = MultiDict()
        fields.update(request.form)
        fields.update(request.files)
        _, data_files = encode_multipart(fields, boundary)
        data += data_files
    elif request.mimetype == "application/x-www-form-urlencoded":
        data += urlencode(list(request.form.items(multi=True))).encode("utf-8")
    return data

For a form content-type, Werkzeug's request.data is empty (the body is reserved for form parsing), so the method != "POST" early-return yields b"" for PUT/PATCH instead of re-encoding request.form + request.files.

Reproduction

from rolo.request import Request, restore_payload

BODY = (
    b'--b\r\nContent-Disposition: form-data; name="field"\r\n\r\nval\r\n'
    b'--b\r\nContent-Disposition: form-data; name="file"; filename="f.txt"\r\n'
    b'Content-Type: text/plain\r\n\r\nfilebytes\r\n--b--\r\n'
)

for method in ("POST", "PUT", "PATCH"):
    r = Request(method=method, path="/x",
                headers={"Content-Type": "multipart/form-data; boundary=b"}, body=BODY)
    out = restore_payload(r)
    print(f"{method:5} len={len(out)}")

Output (rolo 0.8.3):

POST  len=174     # reconstructed
PUT   len=0       # body lost
PATCH len=0       # body lost

Impact

Observed end-to-end on LocalStack 2026.06.0 (HTTP API v2 → Lambda AWS_PROXY): a multipart/form-data PUT (and PATCH) is delivered to the Lambda with "body": "" while Content-Length is forwarded, so the handler sees an empty/truncated form (multipart: NextPart: EOF). The same request as POST works. Any rolo-based proxy path for non-POST form uploads is affected.

Suggested fix

Reconstruct for every method that carries a form body, or key off content-type rather than method:

-    if request.method != "POST":
-        return data
+    if request.method not in ("POST", "PUT", "PATCH"):
+        return data

(Or drop the method check entirely and gate solely on request.mimetype, since the reconstruction is a no-op for non-form bodies.)

Notes

  • restore_payload was last touched in #13, which fixed the multipart-vs-urlencoded re-encoding but left the method != "POST" guard in place.
  • Environment: rolo 0.8.3, Werkzeug (as shipped in LocalStack 2026.06.0), Python 3.13.
Dominant language
Python
Stars
16
Forks
1
Avg merge
22m
Merged PRs (30d)
2

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from localstack/rolo

All issues in localstack/rolo

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.