Repository: litestar-org/litestar

Issue:
Enhancement: replace python-multipart with a faster and more stable multipart parser
python-multipart extremely slow, not maintaining (0.0.5 version released at 12 Oct 2018) and have problems with text files (xml, json, csv etc )

need to swap it to baize or some else 
https://github.com/h0rn3t/starlette-opt/commit/96a7a08b54a24650ad331c4f812659609e7a1494


Discussion:
Are there any benchmarks to support this?
25mb xml  in form-data (python-multipart) = 1-2 min parsing (some problems internal problem in parsing algorithm )
25mb xml in form-data (baize) patched starlette = 6 seconds 
BTW im not first who ask to swap this old python-multipart library, also the code of that lib not so good 
@peterschutt @cofin @infohash -- what do you think?

I looked at Baize and the multipart implementation does look good. I am concerned though adding this dependency into the project - its not modular and we are going to get some annoying import issues. I opened an issue in their github -> https://github.com/abersheeran/baize/issues/47

An alternative would be to fork either baize or the python multipart and create our own smaller implementation.

@h0rn3t - how interested are you in contributing to this?
Plenty of prior art out there to guide us in implementing ourselves. 

Flask and Quart both use [werkzeug.sansio.multipart](https://github.com/pallets/werkzeug/blob/main/src/werkzeug/sansio/multipart.py) and the baize implementation seems to be just a copy of that. Falcon, sanic and django all roll their own implementations as well.
Aight, I'll check this out. Might be an interesting case
> 25mb xml in form-data (python-multipart) = 1-2 min parsing (some problems internal problem in parsing algorithm ) 25mb xml in form-data (baize) patched starlette = 6 seconds

I didn't even think I could be so fast...thanks mypyc


> Plenty of prior art out there to guide us in implementing ourselves.
> 
> Flask and Quart both use [werkzeug.sansio.multipart](https://github.com/pallets/werkzeug/blob/main/src/werkzeug/sansio/multipart.py) and the baize implementation seems to be just a copy of that. Falcon, sanic and django all roll their own implementations as well.

Yes, it's better to build the parser in starlite, which means some targeted optimizations can be made. The multipart format is an old, stable construct that requires little maintenance once the code is written.
I met with the problem of long file uploads too. And it was connected to the old python-multipart library. I was surprised that such an old library without proper support for async is still used in fastapi (using many dependencies from Starlette)
I started work on a library to handle this. I will be using Werkzeug as a basis. 
> I met with the problem of long file uploads too. And it was connected to the old python-multipart library. I was surprised that such an old library without proper support for async is still used in fastapi (using many dependencies from Starlette)

https://github.com/encode/starlette/pull/1514#issuecomment-1041362805
@h0rn3t -- I created a package, adapting werkzeug and using your PR to starlette as a basis. See: https://github.com/starlite-api/starlite-multipart

Would you like to get involved?

Resolving patch:
diff --git a/starlite/connection.py b/starlite/connection.py
--- a/starlite/connection.py
+++ b/starlite/connection.py
@@ -1,4 +1,15 @@
-from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, TypeVar, Union, cast
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Dict,
+    Generic,
+    Optional,
+    Tuple,
+    TypeVar,
+    Union,
+    cast,
+)
+from urllib.parse import parse_qsl
 
 from orjson import OPT_OMIT_MICROSECONDS, OPT_SERIALIZE_NUMPY, dumps, loads
 from starlette.datastructures import URL, URLPath
@@ -6,10 +17,15 @@
 from starlette.requests import empty_receive, empty_send
 from starlette.websockets import WebSocket as StarletteWebSocket
 from starlette.websockets import WebSocketState
+from starlite_multipart import MultipartFormDataParser
+from starlite_multipart import UploadFile as MultipartUploadFile
+from starlite_multipart import parse_options_header
 
+from starlite.datastructures import FormMultiDict, UploadFile
+from starlite.enums import RequestEncodingType
 from starlite.exceptions import ImproperlyConfiguredException, InternalServerException
 from starlite.parsers import parse_query_params
-from starlite.types import Empty
+from starlite.types import Empty, EmptyType
 
 if TYPE_CHECKING:
     from pydantic import BaseModel
@@ -184,6 +200,7 @@ class Request(URLMixin, AppMixin, SessionMixin, Generic[User, Auth], AuthMixin[U
     def __init__(self, scope: "Scope", receive: "Receive" = empty_receive, send: "Send" = empty_send):
         super().__init__(scope, receive, send)
         self._json: Any = Empty
+        self._form: Union[FormMultiDict, EmptyType] = Empty  # type: ignore[assignment]
 
     @property
     def method(self) -> "Method":
@@ -193,6 +210,16 @@ def method(self) -> "Method":
         """
         return cast("Method", self.scope["method"])
 
+    @property
+    def content_type(self) -> Tuple[str, Dict[str, str]]:
+        """Parses the request's 'Content-Type' header, returning the header
+        value and any options as a dictionary.
+
+        Returns:
+            A tuple with the parsed value and a dictionary containing any options send in it.
+        """
+        return parse_options_header(self.headers.get("Content-Type"))
+
     async def json(self) -> Any:
         """Method to retrieve the json request body from the request.
 
@@ -209,6 +236,47 @@ async def json(self) -> Any:
             self._json = loads(body)
         return self._json
 
+    async def form(self) -> FormMultiDict:  # type: ignore[override]
+        """Method to retrieve form data from the request. If the request is
+        either a 'multipart/form-data' or an 'application/x-www-form-
+        urlencoded', this method will return a FormData instance populated with
+        the values sent in the request. Otherwise, an empty instance is
+        returned.
+
+        Returns:
+            A FormData instance.
+        """
+        if self._form is Empty:
+            content_type, options = self.content_type
+            if content_type == RequestEncodingType.MULTI_PART:
+                parser = MultipartFormDataParser(headers=self.headers, stream=self.stream(), max_file_size=None)
+                form_values = await parser()
+                form_values = [
+                    (
+                        k,
+                        UploadFile(
+                            filename=v.filename,
+                            content_type=v.content_type,
+                            headers=v.headers,
+                            file=v.file,  # type: ignore[arg-type]
+                        )
+                        if isinstance(v, MultipartUploadFile)
+                        else v,
+                    )
+                    for k, v in form_values
+                ]
+                self._form = FormMultiDict(form_values)
+
+            elif content_type == RequestEncodingType.URL_ENCODED:
+                self._form = FormMultiDict(
+                    parse_qsl(
+                        b"".join([chunk async for chunk in self.stream()]).decode(options.get("charset", "latin-1"))
+                    )
+                )
+            else:
+                self._form = FormMultiDict()
+        return cast("FormMultiDict", self._form)
+
 
 class WebSocket(  # type: ignore[misc]
     URLMixin, AppMixin, SessionMixin, Generic[User, Auth], AuthMixin[User, Auth], QueryParamMixin, StarletteWebSocket
diff --git a/starlite/datastructures.py b/starlite/datastructures.py
--- a/starlite/datastructures.py
+++ b/starlite/datastructures.py
@@ -28,11 +28,12 @@
 from pydantic_openapi_schema.v3_1_0 import Header
 from starlette.background import BackgroundTask as StarletteBackgroundTask
 from starlette.background import BackgroundTasks as StarletteBackgroundTasks
+from starlette.datastructures import ImmutableMultiDict
 from starlette.datastructures import State as StarletteStateClass
-from starlette.datastructures import UploadFile as StarletteUploadFile
 from starlette.responses import FileResponse, RedirectResponse
 from starlette.responses import Response as StarletteResponse
 from starlette.responses import StreamingResponse
+from starlite_multipart.datastructures import UploadFile as MultipartUploadFile
 from typing_extensions import Literal, ParamSpec
 
 from starlite.openapi.enums import OpenAPIType
@@ -341,7 +342,7 @@ def validate_value(cls, value: Any, values: Dict[str, Any]) -> Any:  # pylint: d
         raise ValueError("value must be set if documentation_only is false")
 
 
-class UploadFile(StarletteUploadFile):
+class UploadFile(MultipartUploadFile):
     @classmethod
     def __modify_schema__(cls, field_schema: Dict[str, Any], field: Optional["ModelField"]) -> None:
         """Creates a pydantic JSON schema.
@@ -355,3 +356,15 @@ def __modify_schema__(cls, field_schema: Dict[str, Any], field: Optional["ModelF
         """
         if field:
             field_schema.update({"type": OpenAPIType.STRING.value, "contentMediaType": "application/octet-stream"})
+
+
+class FormMultiDict(ImmutableMultiDict[str, Any]):
+    async def close(self) -> None:
+        """Closes all files in the multi-dict.
+
+        Returns:
+            None
+        """
+        for _, value in self.multi_items():
+            if isinstance(value, UploadFile):
+                await value.close()
diff --git a/starlite/parsers.py b/starlite/parsers.py
--- a/starlite/parsers.py
+++ b/starlite/parsers.py
@@ -5,7 +5,7 @@
 
 from orjson import JSONDecodeError, loads
 from pydantic.fields import SHAPE_LIST, SHAPE_SINGLETON
-from starlette.datastructures import UploadFile as StarletteUploadFile
+from starlite_multipart.datastructures import UploadFile as MultipartUploadFile
 
 from starlite.datastructures import UploadFile
 from starlite.enums import RequestEncodingType
@@ -14,9 +14,10 @@
     from typing import Union
 
     from pydantic.fields import ModelField
-    from starlette.datastructures import FormData
     from starlette.requests import HTTPConnection
 
+    from starlite.datastructures import FormMultiDict
+
 _true_values = {"True", "true"}
 _false_values = {"False", "false"}
 
@@ -55,7 +56,7 @@ def parse_query_params(connection: "HTTPConnection") -> Dict[str, Any]:
     )
 
 
-def parse_form_data(media_type: "RequestEncodingType", form_data: "FormData", field: "ModelField") -> Any:
+def parse_form_data(media_type: "RequestEncodingType", form_data: "FormMultiDict", field: "ModelField") -> Any:
     """Transforms the multidict into a regular dict, try to load json on all
     non-file values.
 
@@ -63,13 +64,9 @@ def parse_form_data(media_type: "RequestEncodingType", form_data: "FormData", fi
     """
     values_dict: Dict[str, Any] = {}
     for key, value in form_data.multi_items():
-        if not isinstance(value, (UploadFile, StarletteUploadFile)):
+        if not isinstance(value, MultipartUploadFile):
             with suppress(JSONDecodeError):
                 value = loads(value)
-        if isinstance(value, StarletteUploadFile) and not isinstance(value, UploadFile):
-            value = UploadFile(
-                filename=value.filename, file=value.file, content_type=value.content_type, headers=value.headers
-            )
         if values_dict.get(key):
             if isinstance(values_dict[key], list):
                 values_dict[key].append(value)
@@ -80,6 +77,6 @@ def parse_form_data(media_type: "RequestEncodingType", form_data: "FormData", fi
     if media_type == RequestEncodingType.MULTI_PART:
         if field.shape is SHAPE_LIST:
             return list(values_dict.values())
-        if field.shape is SHAPE_SINGLETON and field.type_ in [UploadFile, StarletteUploadFile] and values_dict:
+        if field.shape is SHAPE_SINGLETON and field.type_ in [UploadFile, MultipartUploadFile] and values_dict:
             return list(values_dict.values())[0]
     return values_dict


Test patch:
diff --git a/tests/kwargs/test_multipart_data.py b/tests/kwargs/test_multipart_data.py
--- a/tests/kwargs/test_multipart_data.py
+++ b/tests/kwargs/test_multipart_data.py
@@ -1,11 +1,12 @@
+from os import path
 from typing import Any, Dict, List, Type
 
 import pytest
 from pydantic import BaseConfig, BaseModel
 from starlette.status import HTTP_201_CREATED
+from starlite_multipart.datastructures import UploadFile
 
-from starlite import Body, RequestEncodingType, post
-from starlite.datastructures import UploadFile
+from starlite import Body, Request, RequestEncodingType, post
 from starlite.testing import create_test_client
 from tests import Person, PersonFactory
 from tests.kwargs import Form
@@ -20,6 +21,62 @@ class Config(BaseConfig):
         arbitrary_types_allowed = True
 
 
+@post("/form")
+async def form_handler(request: Request) -> Dict[str, Any]:
+    data = await request.form()
+    output = {}
+    for key, value in data.items():
+        if isinstance(value, UploadFile):
+            content = await value.read()
+            output[key] = {
+                "filename": value.filename,
+                "content": content.decode(),
+                "content_type": value.content_type,
+            }
+        else:
+            output[key] = value
+    return output
+
+
+@post("/form")
+async def form_multi_item_handler(request: Request) -> Dict[str, Any]:
+    data = await request.form()
+    output: Dict[str, list] = {}
+    for key, value in data.multi_items():
+        if key not in output:
+            output[key] = []
+        if isinstance(value, UploadFile):
+            content = await value.read()
+            output[key].append(
+                {
+                    "filename": value.filename,
+                    "content": content.decode(),
+                    "content_type": value.content_type,
+                }
+            )
+        else:
+            output[key].append(value)
+    return output
+
+
+@post("/form")
+async def form_with_headers_handler(request: Request) -> Dict[str, Any]:
+    data = await request.form()
+    output = {}
+    for key, value in data.items():
+        if isinstance(value, UploadFile):
+            content = await value.read()
+            output[key] = {
+                "filename": value.filename,
+                "content": content.decode(),
+                "content_type": value.content_type,
+                "headers": list(value.headers.items()),
+            }
+        else:
+            output[key] = value
+    return output
+
+
 @pytest.mark.parametrize("t_type", [FormData, Dict[str, UploadFile], List[UploadFile], UploadFile])
 def test_request_body_multi_part(t_type: Type[Any]) -> None:
     body = Body(media_type=RequestEncodingType.MULTI_PART)
@@ -47,7 +104,7 @@ class Config(BaseConfig):
         tags: List[str]
         profile: Person
 
-    @post(path="/")
+    @post(path="/form")
     async def test_method(data: MultiPartFormWithMixedFields = Body(media_type=RequestEncodingType.MULTI_PART)) -> None:
         assert await data.image.read() == b"data"
         assert data.tags == ["1", "2", "3"]
@@ -55,8 +112,280 @@ async def test_method(data: MultiPartFormWithMixedFields = Body(media_type=Reque
 
     with create_test_client(test_method) as client:
         response = client.post(
-            "/",
+            "/form",
             files={"image": ("image.png", b"data")},
             data=[("tags", "1"), ("tags", "2"), ("tags", "3"), ("profile", person.json())],
         )
         assert response.status_code == HTTP_201_CREATED
+
+
+def test_multipart_request_files(tmpdir: Any) -> None:
+    path1 = path.join(tmpdir, "test.txt")
+    with open(path1, "wb") as file:
+        file.write(b"<file content>")
+
+    with create_test_client(form_handler) as client, open(path1, "rb") as f:
+        response = client.post("/form", files={"test": f})
+        assert response.json() == {
+            "test": {
+                "filename": "test.txt",
+                "content": "<file content>",
+                "content_type": "",
+            }
+        }
+
+
+def test_multipart_request_files_with_content_type(tmpdir: Any) -> None:
+    path1 = path.join(tmpdir, "test.txt")
+    with open(path1, "wb") as file:
+        file.write(b"<file content>")
+
+    with create_test_client(form_handler) as client, open(path1, "rb") as f:
+        response = client.post("/form", files={"test": ("test.txt", f, "text/plain")})
+        assert response.json() == {
+            "test": {
+                "filename": "test.txt",
+                "content": "<file content>",
+                "content_type": "text/plain",
+            }
+        }
+
+
+def test_multipart_request_multiple_files(tmpdir: Any) -> None:
+    path1 = path.join(tmpdir, "test1.txt")
+    with open(path1, "wb") as file:
+        file.write(b"<file1 content>")
+
+    path2 = path.join(tmpdir, "test2.txt")
+    with open(path2, "wb") as file:
+        file.write(b"<file2 content>")
+
+    with create_test_client(form_handler) as client, open(path1, "rb") as f1, open(path2, "rb") as f2:
+        response = client.post("/form", files={"test1": f1, "test2": ("test2.txt", f2, "text/plain")})  # type: ignore
+        assert response.json() == {
+            "test1": {
+                "filename": "test1.txt",
+                "content": "<file1 content>",
+                "content_type": "",
+            },
+            "test2": {
+                "filename": "test2.txt",
+                "content": "<file2 content>",
+                "content_type": "text/plain",
+            },
+        }
+
+
+def test_multipart_request_multiple_files_with_headers(tmpdir: Any) -> None:
+    path1 = path.join(tmpdir, "test1.txt")
+    with open(path1, "wb") as file:
+        file.write(b"<file1 content>")
+
+    path2 = path.join(tmpdir, "test2.txt")
+    with open(path2, "wb") as file:
+        file.write(b"<file2 content>")
+
+    with create_test_client(form_with_headers_handler) as client, open(path1, "rb") as f1, open(path2, "rb") as f2:
+        response = client.post(
+            "/form",
+            files=[
+                ("test1", (None, f1)),
+                ("test2", ("test2.txt", f2, "text/plain", {"x-custom": "f2"})),
+            ],
+        )
+        assert response.json() == {
+            "test1": "<file1 content>",
+            "test2": {
+                "filename": "test2.txt",
+                "content": "<file2 content>",
+                "content_type": "text/plain",
+                "headers": [
+                    [
+                        "Content-Disposition",
+                        'form-data; name="test2"; filename="test2.txt"',
+                    ],
+                    ["Content-Type", "text/plain"],
+                    ["x-custom", "f2"],
+                ],
+            },
+        }
+
+
+def test_multi_items(tmpdir: Any) -> None:
+    path1 = path.join(tmpdir, "test1.txt")
+    with open(path1, "wb") as file:
+        file.write(b"<file1 content>")
+
+    path2 = path.join(tmpdir, "test2.txt")
+    with open(path2, "wb") as file:
+        file.write(b"<file2 content>")
+
+    with create_test_client(form_multi_item_handler) as client, open(path1, "rb") as f1, open(path2, "rb") as f2:
+        response = client.post(
+            "/form",
+            data=[("test1", "abc")],
+            files=[("test1", f1), ("test1", ("test2.txt", f2, "text/plain"))],
+        )
+        assert response.json() == {
+            "test1": [
+                "abc",
+                {
+                    "filename": "test1.txt",
+                    "content": "<file1 content>",
+                    "content_type": "",
+                },
+                {
+                    "filename": "test2.txt",
+                    "content": "<file2 content>",
+                    "content_type": "text/plain",
+                },
+            ]
+        }
+
+
+def test_multipart_request_mixed_files_and_data() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post(
+            "/form",
+            data=(
+                # data
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n"
+                b'Content-Disposition: form-data; name="field0"\r\n\r\n'
+                b"value0\r\n"
+                # file
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n"
+                b'Content-Disposition: form-data; name="file"; filename="file.txt"\r\n'
+                b"Content-Type: text/plain\r\n\r\n"
+                b"<file content>\r\n"
+                # data
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n"
+                b'Content-Disposition: form-data; name="field1"\r\n\r\n'
+                b"value1\r\n"
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n"
+            ),
+            headers={"Content-Type": ("multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c")},
+        )
+        assert response.json() == {
+            "file": {
+                "filename": "file.txt",
+                "content": "<file content>",
+                "content_type": "text/plain",
+            },
+            "field0": "value0",
+            "field1": "value1",
+        }
+
+
+def test_multipart_request_with_charset_for_filename() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post(
+            "/form",
+            data=(
+                # file
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n"
+                b'Content-Disposition: form-data; name="file"; filename="\xe6\x96\x87\xe6\x9b\xb8.txt"\r\n'  # noqa: E501
+                b"Content-Type: text/plain\r\n\r\n"
+                b"<file content>\r\n"
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n"
+            ),
+            headers={"Content-Type": ("multipart/form-data; charset=utf-8; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c")},
+        )
+        assert response.json() == {
+            "file": {
+                "filename": "文書.txt",
+                "content": "<file content>",
+                "content_type": "text/plain",
+            }
+        }
+
+
+def test_multipart_request_without_charset_for_filename() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post(
+            "/form",
+            data=(
+                # file
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n"
+                b'Content-Disposition: form-data; name="file"; filename="\xe7\x94\xbb\xe5\x83\x8f.jpg"\r\n'  # noqa: E501
+                b"Content-Type: image/jpeg\r\n\r\n"
+                b"<file content>\r\n"
+                b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n"
+            ),
+            headers={"Content-Type": ("multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c")},
+        )
+        assert response.json() == {
+            "file": {
+                "filename": "画像.jpg",
+                "content": "<file content>",
+                "content_type": "image/jpeg",
+            }
+        }
+
+
+def test_multipart_request_with_encoded_value() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post(
+            "/form",
+            data=(
+                b"--20b303e711c4ab8c443184ac833ab00f\r\n"
+                b"Content-Disposition: form-data; "
+                b'name="value"\r\n\r\n'
+                b"Transf\xc3\xa9rer\r\n"
+                b"--20b303e711c4ab8c443184ac833ab00f--\r\n"
+            ),
+            headers={"Content-Type": ("multipart/form-data; charset=utf-8; boundary=20b303e711c4ab8c443184ac833ab00f")},
+        )
+        assert response.json() == {"value": "Transférer"}
+
+
+def test_urlencoded_request_data() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post("/form", data={"some": "data"})
+        assert response.json() == {"some": "data"}
+
+
+def test_no_request_data() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post("/form")
+        assert response.json() == {}
+
+
+def test_urlencoded_percent_encoding() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post("/form", data={"some": "da ta"})
+        assert response.json() == {"some": "da ta"}
+
+
+def test_urlencoded_percent_encoding_keys() -> None:
+    with create_test_client(form_handler) as client:
+        response = client.post("/form", data={"so me": "data"})
+        assert response.json() == {"so me": "data"}
+
+
+def test_postman_multipart_form_data() -> None:
+    postman_body = b'----------------------------850116600781883365617864\r\nContent-Disposition: form-data; name="attributes"; filename="test-attribute_5.tsv"\r\nContent-Type: text/tab-separated-values\r\n\r\n"Campaign ID"\t"Plate Set ID"\t"No"\n\r\n----------------------------850116600781883365617864\r\nContent-Disposition: form-data; name="fasta"; filename="test-sequence_correct_5.fasta"\r\nContent-Type: application/octet-stream\r\n\r\n>P23G01_IgG1-1411:H:Q10C3:1/1:NID18\r\nCAGGTATTGAA\r\n\r\n----------------------------850116600781883365617864--\r\n'  # noqa: E501
+    postman_headers = {
+        "Content-Type": "multipart/form-data; boundary=--------------------------850116600781883365617864",  # noqa: E501
+        "user-agent": "PostmanRuntime/7.26.0",
+        "accept": "*/*",
+        "cache-control": "no-cache",
+        "host": "10.0.5.13:80",
+        "accept-encoding": "gzip, deflate, br",
+        "connection": "keep-alive",
+        "content-length": "2455",
+    }
+
+    with create_test_client(form_handler) as client:
+        response = client.post("/form", data=postman_body, headers=postman_headers)
+        assert response.json() == {
+            "attributes": {
+                "filename": "test-attribute_5.tsv",
+                "content": '"Campaign ID"\t"Plate Set ID"\t"No"\n',
+                "content_type": "text/tab-separated-values",
+            },
+            "fasta": {
+                "filename": "test-sequence_correct_5.fasta",
+                "content": ">P23G01_IgG1-1411:H:Q10C3:1/1:NID18\r\nCAGGTATTGAA\r\n",
+                "content_type": "application/octet-stream",
+            },
+        }