diff --git a/src/requests/utils.py b/src/requests/utils.py index 4d3039b200..5d5035af58 100644 --- a/src/requests/utils.py +++ b/src/requests/utils.py @@ -186,13 +186,30 @@ def super_len(o): if hasattr(o, "seek") and total_length is None: # StringIO and BytesIO have seek but no usable fileno try: - # seek to end of file - o.seek(0, 2) - total_length = o.tell() - - # seek back to current position to support - # partially read file-like objects - o.seek(current_position or 0) + if isinstance(o, io.StringIO): + # StringIO.tell() returns the character + # position, not the byte offset. Read the + # remaining text and encode it to measure the + # true byte length for Content-Length. + start = current_position or 0 + o.seek(start) + total_length = len(o.read().encode("utf-8")) + + # Reset current_position so the returned value + # is just total_length (the remaining bytes). + current_position = 0 + + # seek back to original position to support + # partially read file-like objects + o.seek(start) + else: + # seek to end of file + o.seek(0, 2) + total_length = o.tell() + + # seek back to current position to support + # partially read file-like objects + o.seek(current_position or 0) except OSError: total_length = 0 diff --git a/tests/test_utils.py b/tests/test_utils.py index f9a287af1b..f2d2072d25 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -142,6 +142,30 @@ def test_super_len_with_tell(self): foo.read(2) assert super_len(foo) == 3 + def test_super_len_stringio_multibyte(self): + """Ensure StringIO with multi-byte characters returns the UTF-8 + byte length rather than the character count. See #6917.""" + # Single emoji: 1 character, 4 bytes in UTF-8 + foo = StringIO.StringIO("\U0001F4A9") + assert super_len(foo) == 4 + + # Mixed ASCII and multi-byte + foo = StringIO.StringIO("hello \U0001F4A9 world") + assert super_len(foo) == len("hello \U0001F4A9 world".encode("utf-8")) + + # Partially read StringIO with multi-byte characters + foo = StringIO.StringIO("hello \U0001F4A9 world") + foo.read(6) # read "hello " + remaining_bytes = len("\U0001F4A9 world".encode("utf-8")) + assert super_len(foo) == remaining_bytes + + # Position should be preserved after super_len call + foo = StringIO.StringIO("hello \U0001F4A9 world") + foo.read(3) + pos_before = foo.tell() + super_len(foo) + assert foo.tell() == pos_before + def test_super_len_with_fileno(self): with open(__file__, "rb") as f: length = super_len(f)