Repository: locustio/locust

Issue:
Content-Type with version information is not properly parsed
### Prerequisites

- [X] I am using [the latest version of Locust](https://github.com/locustio/locust/releases/)
- [X] I am reporting a bug, not asking a question

### Description

My app API uses versioning (Microsoft.AspNetCore.Mvc.Versioning) with version info in the media type Content-Type header).
Thus, the responses are like this:
```
Content-Type: application/json; charset=utf-8; api-version=3.0
```

however, the FastResponse.text doesn't expect that Content-Type can have anything else after the `charset=utf-8;` and uses the rest of the value as content encoding.

```
self.encoding = self.headers.get("content-type", "").partition("charset=")[2] or "utf-8"
```

Getting this error:

```
LookupError: unknown encoding: utf-8; api-version=3.0

[2023-10-06 19:37:47,444] Andrews-Mac-Studio.local/ERROR/locust.user.task: unknown encoding: utf-8; api-version=3.0
Traceback (most recent call last):
  File "/opt/homebrew/lib/python3.11/site-packages/locust/user/task.py", line 347, in run
    self.execute_next_task()
  File "/opt/homebrew/lib/python3.11/site-packages/locust/user/task.py", line 378, in execute_next_task
    self.execute_task(self._task_queue.pop(0))
  File "/opt/homebrew/lib/python3.11/site-packages/locust/user/task.py", line 499, in execute_task
    task(self.user)
  File "/Users/andrew/Dev/temp.py", line 10, in index
    with self.rest("POST",
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 137, in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/locust/contrib/fasthttp.py", line 379, in rest
    if resp.text is None:
       ^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/locust/contrib/fasthttp.py", line 465, in text
```


### Command line

locust -f temp.py --headless --host http://localhost:8888 -u 1 -r 1   

### Locustfile contents

```python3
from locust import FastHttpUser, between, task

class WebsiteUser(FastHttpUser):
    @task
    def index(self):
        with self.rest("POST", 
                       "/versioned-api") as res:
            pass
        self.stop()
```


### Python version

Python 3.11.5

### Locust version

locust 2.17.0 from /opt/homebrew/lib/python3.11/site-packages/locust (python 3.11.5)

### Operating system

Mac OS 14.0 (23A344)


Discussion:

Resolving patch:
diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py
--- a/locust/contrib/fasthttp.py
+++ b/locust/contrib/fasthttp.py
@@ -22,6 +22,9 @@
 from geventhttpclient.response import HTTPConnectionClosed, HTTPSocketPoolResponse
 from geventhttpclient.header import Headers
 
+# borrow requests's content-type header parsing
+from requests.utils import get_encoding_from_headers
+
 from locust.user import User
 from locust.exception import LocustError, CatchResponseError, ResponseError
 from locust.env import Environment
@@ -460,7 +463,7 @@ def text(self) -> Optional[str]:
             if self.headers is None:
                 self.encoding = "utf-8"
             else:
-                self.encoding = self.headers.get("content-type", "").partition("charset=")[2] or "utf-8"
+                self.encoding = get_encoding_from_headers(self.headers)
         return str(self.content, self.encoding, errors="replace")
 
     @property


Test patch:
diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py
--- a/locust/test/test_fasthttp.py
+++ b/locust/test/test_fasthttp.py
@@ -423,6 +423,16 @@ class MyUser(FastHttpUser):
         locust = MyUser(self.environment)
         self.assertEqual(200, locust.client.head("/request_method").status_code)
 
+    def test_complex_content_type(self):
+        class MyUser(FastHttpUser):
+            host = "http://127.0.0.1:%i" % self.port
+
+        locust = MyUser(self.environment)
+
+        self.assertEqual("stuff", locust.client.get("/content_type_missing_charset").text)
+        self.assertEqual("stuff", locust.client.get("/content_type_regular").text)
+        self.assertEqual("stuff", locust.client.get("/content_type_with_extra_stuff").text)
+
     def test_log_request_name_argument(self):
         self.response = ""
 
diff --git a/locust/test/testcases.py b/locust/test/testcases.py
--- a/locust/test/testcases.py
+++ b/locust/test/testcases.py
@@ -149,6 +149,27 @@ def rest():
     return request.json
 
 
+@app.route("/content_type_missing_charset")
+def content_type_missing_charset():
+    resp = make_response("stuff")
+    resp.headers["Content-Type"] = "Content-Type: application/json;"
+    return resp
+
+
+@app.route("/content_type_regular")
+def content_type_regular():
+    resp = make_response("stuff")
+    resp.headers["Content-Type"] = "Content-Type: application/json; charset=utf-8;"
+    return resp
+
+
+@app.route("/content_type_with_extra_stuff")
+def content_type_with_extra_stuff():
+    resp = make_response("stuff")
+    resp.headers["Content-Type"] = "Content-Type: application/json; charset=utf-8; api-version=3.0"
+    return resp
+
+
 class LocustTestCase(unittest.TestCase):
     """
     Test case class that restores locust.events.EventHook listeners on tearDown, so that it is