almarklein: Removed old mechanism. Tests are failing in a few places, as expected. Now to put in the new mechanism.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/28809835/badge)](https://coveralls.io/builds/28809835)

Coverage decreased (-0.2%) to 89.259% when pulling **3e161056c3d314ed361502b5b8005a99bbc48691 on seek** into **dc212e26c4fdd662f195c017e34f03b184ea6ab0 on master**.

diff --git a/imageio/core/request.py b/imageio/core/request.py
index a4a68bf..dbd455b 100644
--- a/imageio/core/request.py
+++ b/imageio/core/request.py
@@ -340,12 +340,12 @@ class Request(object):
                 # Open zipfile and open new file object for specific file
                 self._zipfile = zipfile.ZipFile(filename, "r")
                 self._file = self._zipfile.open(name, "r")
-                make_file_object_support_noop_seeks(self._file)
+                self._file = SeekableFileObject(self._file)
 
         elif self._uri_type in [URI_HTTP or URI_FTP]:
             assert not want_to_write  # This should have been tested in init
             self._file = urlopen(self.filename, timeout=5)
-            make_file_object_support_noop_seeks(self._file)
+            self._file = SeekableFileObject(self._file)
 
         return self._file
 
@@ -482,33 +482,90 @@ def read_n_bytes(f, N):
     return bb
 
 
-def make_file_object_support_noop_seeks(f):
-    """This fixes up an HTTPResponse object so that it can tell(), and also
-    seek() will work if its effectively a no-op. This allows tools like Pillow
-    to use the file object.
+class SeekableFileObject:
+    """ A readonly wrapper file object that add support for seeking, even if
+    the wrapped file object does not. The allows us to stream from http and
+    still use Pillow.
     """
-    count = [0]
 
-    def read(n=None):
-        res = ori_read(n)
-        count[0] += len(res)
+    def __init__(self, f):
+        self.f = f
+        self._i = 0  # >=0 but can exceed buffer
+        self._buffer = b""
+        self._have_all = False
+        self.closed = False
+
+    def read(self, n=None):
+
+        # Fix up n
+        if n is None:
+            pass
+        else:
+            n = int(n)
+            if n < 0:
+                n = None
+
+        # Can and must we read more?
+        if not self._have_all:
+            more = b""
+            if n is None:
+                more = self.f.read()
+                self._have_all = True
+            else:
+                want_i = self._i + n
+                want_more = want_i - len(self._buffer)
+                if want_more > 0:
+                    more = self.f.read(want_more)
+                    if len(more) < want_more:
+                        self._have_all = True
+            self._buffer += more
+
+        # Read data from buffer and update pointer
+        if n is None:
+            res = self._buffer[self._i :]
+        else:
+            res = self._buffer[self._i : self._i + n]
+        self._i += len(res)
+
         return res
 
-    def tell():
-        return count[0]
+    def tell(self):
+        return self._i
+
+    def seek(self, i, mode=0):
+        # Mimic BytesIO behavior
+
+        # Get the absolute new position
+        i = int(i)
+        if mode == 0:
+            if i < 0:
+                raise ValueError("negative seek value " + str(i))
+            real_i = i
+        elif mode == 1:
+            real_i = max(0, self._i + i)  # negative ok here
+        elif mode == 2:
+            if not self._have_all:
+                self.read()
+            real_i = max(0, len(self._buffer) + i)
+        else:
+            raise ValueError("invalid whence (%s, should be 0, 1 or 2)" % i)
+
+        # Read some?
+        if real_i <= len(self._buffer):
+            pass  # no need to read
+        elif not self._have_all:
+            assert real_i > self._i  # if we don't have all, _i cannot be > _buffer
+            self.read(real_i - self._i)  # sets self._i
 
-    def seek(i, mode=0):
-        if not (mode == 0 and i == count[0]):
-            ori_seek(i, mode)
+        self._i = real_i
+        return self._i
 
-    def fail_seek(i, mode=0):
-        raise RuntimeError("No seeking allowed!")
+    def close(self):
+        self.closed = True
+        self.f.close()
 
-    # Note, there is currently no protection from wrapping an object more than
-    # once, it will (probably) work though, because closures.
-    ori_read = f.read
-    ori_seek = f.seek if hasattr(f, "seek") else fail_seek
+    def isatty(self):
+        return False
 
-    f.read = read
-    f.tell = tell
-    f.seek = seek
+    def seekable(self):
+        return True

UnsupportedOperation seek while reading GIF from HTTP uri
Related to:
https://github.com/imageio/imageio/issues/202
https://github.com/imageio/imageio/issues/238
https://github.com/imageio/imageio/issues/335

Reading GIF images from a HTTP fails with `"UnsupportedOperation: seek"`

For example, try running:
```im = imageio.mimread('http://media.giphy.com/media/3oD3YveOJWdwIAfZ5e/giphy.gif')```

Python 3.7.6
Imageio 2.6.1
Pillow 7.0.0
diff --git a/tests/test_core.py b/tests/test_core.py
index ca41011..73a9001 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,14 +1,15 @@
 """ Test imageio core functionality.
 """
 
-import time
-import sys
 import os
+import sys
+import time
 import shutil
+import random
+import tempfile
 import ctypes.util
-from zipfile import ZipFile
 from io import BytesIO
-import tempfile
+from zipfile import ZipFile
 
 import numpy as np
 import pytest
@@ -306,6 +307,60 @@ def test_request_save_sources():
         assert res == bytes
 
 
+def test_request_seekable_file_object():
+    SeekableFileObject = imageio.core.request.SeekableFileObject
+    data = bytes([int(random.uniform(0, 255)) for i in range(100)])
+    f1 = BytesIO(data)
+    f2 = SeekableFileObject(f1)
+
+    # Test all kinds of seeks, reads and tells
+    resses = []
+    for f in (f1, f2):
+        (f1.seek(0), f2.seek(0))
+        res = [
+            f.read(8),
+            f.tell(),
+            f.seek(0),
+            f.tell(),
+            f.read(8),
+            f.read(),
+            f.seek(20, 1),
+            f.read(8),
+            f.seek(10, 2),
+            f.read(8),
+            f.seek(-10, 2),
+            f.read(8),
+            f.seek(-20, 2),
+            f.tell(),
+            f.read(8),
+            f.read(0),
+            f.read(-1),
+        ]
+        resses.append(res)
+    assert resses[0] == resses[1]
+
+    # Test out of bounds
+    for f in (f1, f2):
+        # Align them
+        (f1.seek(0), f2.seek(0))
+        f.seek(3)
+        assert f.tell() == 3
+        with raises(ValueError):
+            f.seek(-10)
+        assert f.tell() == 3
+        f.seek(-10, 1)  # no raise!
+        assert f.tell() == 0
+        f.seek(-10, 2)
+        assert f.tell() == 90
+        f.seek(10, 2)
+        assert f.tell() == 110
+        assert f.read(1) == b""
+        f.seek(20, 1)
+        assert f.tell() == 130
+        f.seek(150)
+        assert f.tell() == 150
+
+
 def test_request_file_no_seek():
     class File:
         def read(self, n):

pip install -e .[dev]
parse_log_pytest
requirements.txt
apt-get update apt-get install -y libfreeimage3
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
astropy==6.0.1
astropy-iers-data==0.2025.3.31.0.36.18
babel==2.17.0
black==25.1.0
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
coverage==7.8.0
coveralls==4.0.1
docopt==0.6.2
docutils==0.21.2
exceptiongroup==1.2.2
flake8==7.2.0
idna==3.10
-e git+https://github.com/imageio/imageio.git@dc212e26c4fdd662f195c017e34f03b184ea6ab0#egg=imageio
imageio-ffmpeg==0.6.0
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
invoke==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
mccabe==0.7.0
mypy-extensions==1.0.0
numpy==1.26.4
numpydoc==1.8.0
packaging==24.2
pathspec==0.12.1
pillow==11.1.0
platformdirs==4.3.7
pluggy==1.5.0
psutil==7.0.0
pycodestyle==2.13.0
pyerfa==2.0.1.5
pyflakes==3.3.2
Pygments==2.19.1
pytest==8.3.5
pytest-cov==6.0.0
PyYAML==6.0.2
requests==2.32.3
SimpleITK==2.4.1
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tabulate==0.9.0
tomli==2.2.1
typing_extensions==4.13.0
urllib3==2.3.0
zipp==3.21.0

name: imageio
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - astropy==6.0.1
      - astropy-iers-data==0.2025.3.31.0.36.18
      - babel==2.17.0
      - black==25.1.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - click==8.1.8
      - coverage==7.8.0
      - coveralls==4.0.1
      - docopt==0.6.2
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - idna==3.10
      - imageio-ffmpeg==0.6.0
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - invoke==2.2.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - mypy-extensions==1.0.0
      - numpy==1.26.4
      - numpydoc==1.8.0
      - packaging==24.2
      - pathspec==0.12.1
      - pillow==11.1.0
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - psutil==7.0.0
      - pycodestyle==2.13.0
      - pyerfa==2.0.1.5
      - pyflakes==3.3.2
      - pygments==2.19.1
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pyyaml==6.0.2
      - requests==2.32.3
      - simpleitk==2.4.1
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tabulate==0.9.0
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - zipp==3.21.0
prefix: /opt/conda/envs/imageio

tests/test_core.py::test_request_seekable_file_object
tests/test_core.py::test_findlib2 tests/test_core.py::test_functions tests/test_core.py::test_memtest
tests/test_core.py::test_fetching tests/test_core.py::test_request tests/test_core.py::test_request_read_sources tests/test_core.py::test_request_save_sources tests/test_core.py::test_request_file_no_seek tests/test_core.py::test_util_image tests/test_core.py::test_util_dict tests/test_core.py::test_util_get_platform tests/test_core.py::test_util_asarray tests/test_core.py::test_util_progres_bar tests/test_core.py::test_util_image_as_uint tests/test_core.py::test_util_has_has_module tests/test_core.py::test_to_nbytes_correct[1-1_0] tests/test_core.py::test_to_nbytes_correct[1-1_1] tests/test_core.py::test_to_nbytes_correct[8B-8] tests/test_core.py::test_to_nbytes_correct[1MB-1000000] tests/test_core.py::test_to_nbytes_correct[1M-1000000] tests/test_core.py::test_to_nbytes_correct[1GiB-1073741824] tests/test_core.py::test_to_nbytes_correct[1.5TB-1500000000000.0] tests/test_core.py::test_to_nbytes_incorrect[1mb] tests/test_core.py::test_to_nbytes_incorrect[1Giib] tests/test_core.py::test_to_nbytes_incorrect[GB] tests/test_core.py::test_to_nbytes_incorrect[1.3.2TB] tests/test_core.py::test_to_nbytes_incorrect[8b] tests/test_core.py::test_example_plugin tests/test_core.py::test_imwrite_not_subclass tests/test_core.py::test_imwrite_not_array_like
imageio/core/request.py
imageio/core/request.py