Files without a filename should not set a Content-Type in multipart data. (#520)
* File upoads with no filename should not set a Content-Type in their multipart data. * Update type annotations to allow file uploads to be a string
This commit is contained in:
parent
7d45db068b
commit
ed949508a6
@ -81,11 +81,18 @@ RequestData = typing.Union[dict, str, bytes, typing.AsyncIterator[bytes]]
|
|||||||
RequestFiles = typing.Dict[
|
RequestFiles = typing.Dict[
|
||||||
str,
|
str,
|
||||||
typing.Union[
|
typing.Union[
|
||||||
typing.IO[typing.AnyStr], # file
|
# file (or str)
|
||||||
typing.Tuple[str, typing.IO[typing.AnyStr]], # (filename, file)
|
typing.Union[typing.IO[typing.AnyStr], typing.AnyStr],
|
||||||
|
# (filename, file (or str))
|
||||||
typing.Tuple[
|
typing.Tuple[
|
||||||
str, typing.IO[typing.AnyStr], str
|
typing.Optional[str], typing.Union[typing.IO[typing.AnyStr], typing.AnyStr],
|
||||||
], # (filename, file, content_type)
|
],
|
||||||
|
# (filename, file (or str), content_type)
|
||||||
|
typing.Tuple[
|
||||||
|
typing.Optional[str],
|
||||||
|
typing.Union[typing.IO[typing.AnyStr], typing.AnyStr],
|
||||||
|
typing.Optional[str],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -58,19 +58,21 @@ class FileField(Field):
|
|||||||
value[2] if len(value) > 2 else self.guess_content_type()
|
value[2] if len(value) > 2 else self.guess_content_type()
|
||||||
)
|
)
|
||||||
|
|
||||||
def guess_content_type(self) -> str:
|
def guess_content_type(self) -> typing.Optional[str]:
|
||||||
if self.filename:
|
if self.filename:
|
||||||
return mimetypes.guess_type(self.filename)[0] or "application/octet-stream"
|
return mimetypes.guess_type(self.filename)[0] or "application/octet-stream"
|
||||||
else:
|
else:
|
||||||
return "application/octet-stream"
|
return None
|
||||||
|
|
||||||
def render_headers(self) -> bytes:
|
def render_headers(self) -> bytes:
|
||||||
parts = [b"Content-Disposition: form-data; ", _format_param("name", self.name)]
|
parts = [b"Content-Disposition: form-data; ", _format_param("name", self.name)]
|
||||||
if self.filename:
|
if self.filename:
|
||||||
filename = _format_param("filename", self.filename)
|
filename = _format_param("filename", self.filename)
|
||||||
parts.extend([b"; ", filename])
|
parts.extend([b"; ", filename])
|
||||||
|
if self.content_type is not None:
|
||||||
content_type = self.content_type.encode()
|
content_type = self.content_type.encode()
|
||||||
parts.extend([b"\r\nContent-Type: ", content_type, b"\r\n\r\n"])
|
parts.extend([b"\r\nContent-Type: ", content_type])
|
||||||
|
parts.append(b"\r\n\r\n")
|
||||||
return b"".join(parts)
|
return b"".join(parts)
|
||||||
|
|
||||||
def render_data(self) -> bytes:
|
def render_data(self) -> bytes:
|
||||||
|
|||||||
@ -141,13 +141,34 @@ def test_multipart_encode_files_allows_filenames_as_none():
|
|||||||
|
|
||||||
assert content_type == f"multipart/form-data; boundary={boundary}"
|
assert content_type == f"multipart/form-data; boundary={boundary}"
|
||||||
assert body == (
|
assert body == (
|
||||||
'--{0}\r\nContent-Disposition: form-data; name="file"\r\n'
|
'--{0}\r\nContent-Disposition: form-data; name="file"\r\n\r\n'
|
||||||
"Content-Type: application/octet-stream\r\n\r\n<file content>\r\n"
|
"<file content>\r\n--{0}--\r\n"
|
||||||
"--{0}--\r\n"
|
|
||||||
"".format(boundary).encode("ascii")
|
"".format(boundary).encode("ascii")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file_name,expected_content_type",
|
||||||
|
[("example.json", "application/json"), ("example.log", "application/octet-stream")],
|
||||||
|
)
|
||||||
|
def test_multipart_encode_files_guesses_correct_content_type(
|
||||||
|
file_name, expected_content_type
|
||||||
|
):
|
||||||
|
files = {"file": (file_name, io.BytesIO(b"<file content>"))}
|
||||||
|
with mock.patch("os.urandom", return_value=os.urandom(16)):
|
||||||
|
boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
|
||||||
|
|
||||||
|
body, content_type = multipart.multipart_encode(data={}, files=files)
|
||||||
|
|
||||||
|
assert content_type == f"multipart/form-data; boundary={boundary}"
|
||||||
|
assert body == (
|
||||||
|
f'--{boundary}\r\nContent-Disposition: form-data; name="file"; '
|
||||||
|
f'filename="{file_name}"\r\nContent-Type: '
|
||||||
|
f"{expected_content_type}\r\n\r\n<file content>\r\n--{boundary}--\r\n"
|
||||||
|
"".encode("ascii")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_multipart_encode_files_allows_str_content():
|
def test_multipart_encode_files_allows_str_content():
|
||||||
files = {"file": ("test.txt", "<string content>", "text/plain")}
|
files = {"file": ("test.txt", "<string content>", "text/plain")}
|
||||||
with mock.patch("os.urandom", return_value=os.urandom(16)):
|
with mock.patch("os.urandom", return_value=os.urandom(16)):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user