restore_payload drops the body of multipart/form-data and x-www-form-urlencoded PUT/PATCH requests
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 16
- Forks
- 1
- Avg merge
- 22m
- Merged PRs (30d)
- 2
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 `` (which only supports GET/POST), not to HTTP itself.
## Affected code
`rolo/request.py` — `restore_payload` (rolo 0.8.3):
```python
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
```python
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:
```python
- 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.
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.
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.
Assessment
- Tech stack
- python
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100