localstack / localstack/rolo

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.

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

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.

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.