diff --git a/httpie/uploads.py b/httpie/uploads.py index 4a993b3a25..9594eee954 100644 --- a/httpie/uploads.py +++ b/httpie/uploads.py @@ -87,7 +87,16 @@ def wrapped(*args, **kwargs): def is_stdin(file: IO) -> bool: try: file_no = file.fileno() - except Exception: + except (OSError, ValueError, AttributeError): + # fileno() can fail in a few documented ways: + # - AttributeError: object has no fileno attribute (e.g. BytesIO, + # custom streams, anything that doesn't expose a real fd); + # - io.UnsupportedOperation: stream types like StringIO that + # explicitly disallow fileno() (subclass of OSError); + # - ValueError: "I/O operation on closed file" for closed + # file objects; + # - OSError: low-level errors on a corrupted/closed fd. + # In all of these cases the object is not stdin, so return False. return False else: return file_no == sys.stdin.fileno() diff --git a/tests/test_uploads.py b/tests/test_uploads.py index e6bb80ac70..cdec8bacdf 100644 --- a/tests/test_uploads.py +++ b/tests/test_uploads.py @@ -401,3 +401,55 @@ def test_multiple_request_bodies_from_file_by_path(self, httpbin): ) assert r.exit_status == ExitStatus.ERROR assert 'from multiple files' in r.stderr + + +class TestIsStdin: + """`is_stdin` should return False (and not raise) for any object whose + `fileno()` call fails for a documented reason (no attribute, closed file, + or non-fd stream like StringIO).""" + def test_is_stdin_returns_false_for_plain_object_without_fileno(self): + from httpie.uploads import is_stdin + + class NoFileno: + def read(self, *_args, **_kwargs): + return b'' + + assert is_stdin(NoFileno()) is False + + def test_is_stdin_returns_false_for_closed_file(self): + from httpie.uploads import is_stdin + import tempfile + f = tempfile.TemporaryFile() + f.close() + assert is_stdin(f) is False + + def test_is_stdin_returns_false_for_stringio(self): + from httpie.uploads import is_stdin + import io + assert is_stdin(io.StringIO('hello')) is False + + def test_is_stdin_returns_false_for_bytesio(self): + from httpie.uploads import is_stdin + import io + assert is_stdin(io.BytesIO(b'hello')) is False + + def test_is_stdin_returns_false_for_other_real_file(self, monkeypatch): + from httpie.uploads import is_stdin + import io + import os + import tempfile + # Patch sys.stdin with a known surrogate so we control both sides + # of the comparison; pytest's own capture machinery replaces sys.stdin + # with a DontReadFromInput whose fileno() raises io.UnsupportedOperation, + # which is fragile to depend on in a unit test. + stdin_surrogate = io.BytesIO() + stdin_surrogate.fileno = lambda: 4242 # any value that won't collide + monkeypatch.setattr('httpie.uploads.sys.stdin', stdin_surrogate) + # is_stdin(our surrogate) should be True + assert is_stdin(stdin_surrogate) is True + # is_stdin(any other real fd) should be False + r, w = os.pipe() + try: + assert is_stdin(os.fdopen(r, 'r')) is False + finally: + os.close(w)