Repository: elastic/apm-agent-python

Issue:
capture_body makes requests hangs / validation error
**Describe the bug**: 
When using CAPTURE_BODY:
1) "all" - you get "validation error: error: context: request: 'body': validation rule 'types(string;map[string]interface)' violated "
2) "errors" - sending httpx request to unreachable address, the client gets stuck and no repsonse is returned to the client


**To Reproduce**

1. Create FastAPI.
2. do any request using  httpx.AsyncClient to unknown request (ex: localhost: 5050)
3. if your CAPTURE_BODY is set to all, you will get validation error
4. if your CAPTURE_BODY is set to errors, the requests will hang forever

**Environment (please complete the following information)**
- OS: [e.g. Linux]: Docker python:3.8.2-slim-buster
- Python version: 3.8.2
- Framework and version [e.g. Django 2.1]: FastAPI 63.0
- APM Server version: 7.10.2
- Agent version: 5.10.1


**Additional context**

Add any other context about the problem here.

- Agent config options <!-- be careful not to post sensitive information -->
  <details>
    <summary>Click to expand</summary>

    ```
    apm_config = {
        "SERVICE_NAME": "map",
        "ENABLED": True,
        "SERVER_URL": "http://**********:8200",
        "CAPTURE_HEADERS": True,
        "CAPTURE_BODY": "all",
    }
    ```
  </details>
- `requirements.txt`:
  <details>
    <summary>Click to expand</summary>

    ```
    aiofiles
    aredis
    async-timeout
    asyncpg
    authlib
    certifi
    cffi
    chardet
    click
    cryptography
    debugpy
    python-dateutil
    ecdsa<0.15
    elastic-apm
    fastapi
    gunicorn
    h11
    h2
    hiredis
    hpack
    hstspreload
    httpcore
    httptools
    httpx
    hyperframe
    idna
    loguru
    orjson
    pyasn1
    pycparser
    pydantic
    python-jose
    python-multipart
    rfc3986
    rsa
    six
    sniffio
    starlette==0.13.6
    ujson
    uvicorn
    uvloop
    websockets
    # yappi
    ```
  </details>



Discussion:
After debugging a little, it's related to the body payload, it seems flat body like: [10,20] will make it not work
@Syniex To clarify, using a flat body like that will make the request hang? Does it hang even without the agent present? If the agent makes the application hang for any reason, we consider that a bug, even if a request body is malformed.
@basepi  without the agent the application doesn't hangs.
I will provide code tomorrow, and dig deeper to it.

Thank you. This sounds like something we would like to fix.
```python
from typing import List

import httpx
import uvicorn
from elasticapm.contrib.starlette import ElasticAPM, make_apm_client
from fastapi import FastAPI
from pydantic import BaseModel


class Point(BaseModel):
    coordinates: List[float]


app = FastAPI()
apm_config = {
    "SERVICE_NAME": "testing",
    "ENABLED": True,
    "SERVER_URL": "http://localhost:8200",
    "CAPTURE_HEADERS": True,
    "CAPTURE_BODY": "error",
    # "CAPTURE_BODY": "all",
}
apm = make_apm_client(apm_config)
app.add_middleware(ElasticAPM, client=apm)

@app.post("/working")
async def working_fine():
    #Example of working endpoint when no body is provided
    async with httpx.AsyncClient() as client:
        response = await client.get("http://notexisthost/")
        return response.json()


@app.post("/not/working/1")
async def not_working(point: Point):
    # This won't work when using "CAPTURE_BODY": "errors" and will hang the request forever
    async with httpx.AsyncClient() as client:
        response = await client.get("http://notexisthost/")
        return response.json()


@app.post("/not/working/2")
async def not_working_2(coordinates: List[float]):
    # "CAPTURE_BODY": "all" will make elasticsearch says "body": validation error
    async with httpx.AsyncClient() as client:
        response = await client.get("http://notexisthost/")
        return response.json()


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0")
```

@basepi  Please take a look at this example

You can also try to disable the apm middleware and see all endspoints will work as expected.
@basepi I checked the commit and the application doesn't hangs anymore.

The Validation error is still happens.
should i open a new issue?

```python
from typing import List

import uvicorn
from elasticapm.contrib.starlette import ElasticAPM, make_apm_client
from fastapi import FastAPI

app = FastAPI()
apm_config = {
    "SERVICE_NAME": "testing",
    "ENABLED": True,
    "SERVER_URL": "http://localhost:8200",
    "CAPTURE_HEADERS": True,
    "CAPTURE_BODY": "all",
}
apm = make_apm_client(apm_config)
app.add_middleware(ElasticAPM, client=apm)


@app.post("/validation/error")
async def working_fine(coordinates: List[float]):
    # Example of working endpoint when no body is provided
    return "Done"


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", debug=False)

```
<details><summary>Error</summary>

Failed to submit message: 'HTTP 400: {"accepted":0,"errors":[{"message":"validation error: transaction: context: request: \'body\': validation rule \'types(string;map[string]interface)\' violated ","document":"{\\"transaction\\": {\\"id\\": \\"ce58ac1681313731\\", \\"trace_id\\": \\"a7d9f1835b25f09dd333780d54532512\\", \\"name\\": \\"POST /not/working/2\\", \\"type\\": \\"request\\", \\"duration\\": 5.093664978630841, \\"result\\": \\"HTTP 2xx\\", \\"timestamp\\": 1612563016569798, \\"outcome\\": \\"success\\", \\"sampled\\": true, \\"span_count\\": {\\"started\\": 0, \\"dropped\\": 0}, \\"sample_rate\\": 1.0, \\"context\\": {\\"request\\": {\\"method\\": \\"POST\\", \\"socket\\": {\\"remote_address\\": null, \\"encrypted\\": false}, \\"cookies\\": {\\"pga4_session\\": \\"[REDACTED]\\"}, \\"headers\\": {\\"host\\": \\"localhost:8000\\", \\"connection\\": \\"keep-alive\\", \\"content-length\\": \\"5\\", \\"accept\\": \\"application/json\\", \\"user-agent\\": \\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36\\", \\"content-type\\": \\"application/json\\", \\"origin\\": \\"http://localhost:8000\\", \\"referer\\": \\"http://localhost:8000/docs\\", \\"accept-encoding\\": \\"gzip, deflate\\", \\"accept-language\\": \\"en-US,en;q=0.9,he-IL;q=0.8,he;q=0.7,lb;q=0.6\\", \\"cookie\\": \\"pga4_session=511fd9bc-c1ba-4c43-9f8b-0de0afabf0b9!uSyjxu3n1arVr6rAh2dHxBz9Jr4=\\"}, \\"body\\": [0, 0], \\"url\\": {\\"full\\": \\"http://localhost:8000/not/working/2\\", \\"protocol\\": \\"http:\\", \\"hostname\\": \\"localhost\\", \\"pathname\\": \\"/not/working/2\\", \\"port\\": \\"8000\\"}}, \\"response\\": {\\"status_code\\": 200, \\"headers\\": {\\"content-length\\": \\"4\\", \\"content-type\\": \\"application/json\\"}}, \\"tags\\": {}}}}"}]}\n'

</details>

Oh, right, sorry, I got so hung up on the hang (which was definitely the biggest problem) that I forgot to look at the validation issues. Thanks for attaching an example error, that will be helpful.

Resolving patch:
diff --git a/elasticapm/contrib/starlette/utils.py b/elasticapm/contrib/starlette/utils.py
--- a/elasticapm/contrib/starlette/utils.py
+++ b/elasticapm/contrib/starlette/utils.py
@@ -28,8 +28,6 @@
 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-import json
-
 from starlette.requests import Request
 from starlette.responses import Response
 from starlette.types import Message
@@ -64,10 +62,6 @@ async def get_data_from_request(request: Request, config: Config, event_type: st
             body = None
             try:
                 body = await get_body(request)
-                if request.headers.get("content-type") == "application/x-www-form-urlencoded":
-                    body = await query_params_to_dict(body)
-                else:
-                    body = json.loads(body)
             except Exception:
                 pass
             if body is not None:


Test patch:
diff --git a/tests/contrib/asyncio/starlette_tests.py b/tests/contrib/asyncio/starlette_tests.py
--- a/tests/contrib/asyncio/starlette_tests.py
+++ b/tests/contrib/asyncio/starlette_tests.py
@@ -155,7 +155,7 @@ def test_post(app, elasticapm_client):
     request = transaction["context"]["request"]
     assert request["method"] == "POST"
     assert request["socket"] == {"remote_address": "127.0.0.1", "encrypted": False}
-    assert request["body"]["foo"] == "bar"
+    assert request["body"] == "foo=bar"
 
     assert span["name"] == "test"