restore_payload drops the body of multipart/form-data and x-www-form-urlencoded PUT/PATCH requests
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
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.py — restore_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_payloadwas last touched in #13, which fixed the multipart-vs-urlencoded re-encoding but left themethod != "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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from localstack/rolo
-
Difficulty 1/5 Under an hour Newbie friendliness 55/100
localstack/rolo#28 ·
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100