Repository: geopandas/geopandas

Issue:
REG: URL reading limited to HTTP urls
Hi!

I think there is a regression in 0.13.1. Opening files through a non-HTTP url fails. Specifically, I have a program where local files are referred with "file:///path/to/geojson"  URLs. 

MWE:
```python3
from pathlib import Path
import geopandas as gpd
from urllib.request import urlopen

# Free and accessible geojson with the outline of Austria
url = 'https://raw.githubusercontent.com/johan/world.geo.json/master/countries/AUT.geo.json'

# This works
gdf = gpd.read_file(url)

# Download file:
local = Path('AUT.geo.json')
with local.open('wb') as f:
    f.write(urlopen(url).read())

# Reopen local url, fail
gdf = gpd.read_file(f"file://{local.absolute()}")
```
Fails with:
```python3
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[8], line 17
     14     f.write(urlopen(url).read())
     16 # Reopen local url, fail
---> 17 gdf = gpd.read_file(f"file://{local.absolute()}")

File ~/mambaforge/envs/xscen-dev/lib/python3.10/site-packages/geopandas/io/file.py:264, in _read_file(filename, bbox, mask, rows, engine, **kwargs)
    258 if _is_url(filename):
    259     # if it is a url that supports random access -> pass through to
    260     # pyogrio/fiona as is (to support downloading only part of the file)
    261     # otherwise still download manually because pyogrio/fiona don't support
    262     # all types of urls (https://github.com/geopandas/geopandas/issues/2908)
    263     request = urllib.request.urlopen(filename)
--> 264     if not request.getheader("Accept-Ranges") == "bytes":
    265         filename = request.read()
    266         from_bytes = True

File ~/mambaforge/envs/xscen-dev/lib/python3.10/tempfile.py:478, in _TemporaryFileWrapper.__getattr__(self, name)
    473 def __getattr__(self, name):
    474     # Attribute lookups are delegated to the underlying file
    475     # and cached for non-numeric results
    476     # (i.e. methods are cached, closed and friends are not)
    477     file = self.__dict__['file']
--> 478     a = getattr(file, name)
    479     if hasattr(a, '__call__'):
    480         func = a

AttributeError: '_io.BufferedReader' object has no attribute 'getheader'
```

I think this is because a specific type of return was expected from `urlopen` here:
https://github.com/geopandas/geopandas/blob/979730b6fa1fa9a5f8102b6dcb99ec78a627954b/geopandas/io/file.py#L263-L264

In the official Python doc for `urlopen` it says: 
> This function always returns an object which can work as a [context manager](https://docs.python.org/3/glossary.html#term-context-manager) and has the properties url, headers, and status. See [urllib.response.addinfourl](https://docs.python.org/3/library/urllib.request.html?highlight=urlopen#urllib.response.addinfourl) for more detail on these properties.

The `getheader` method exists only on the `HTTPResponse` object, which is indeed returned if the url HTTP. However, it seems we could achieve the same check by accessing the  `headers` property which is always present. I believe the faulty `if` line could be changed to:
```python3
if not request.headers.get("Accept-Ranges") == "bytes": 
```


Discussion:
Ah, I didn't realize that such generic URIs would pass our `_is_url` check (but we actually explicitly check for `file://` in there ..). Thanks for the report! Will do a quick fix and another bug-fix release.

Resolving patch:
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -21,6 +21,8 @@
 
 _VALID_URLS = set(uses_relative + uses_netloc + uses_params)
 _VALID_URLS.discard("")
+# file:// URIs are supported by fiona/pyogrio -> don't already open + read the file here
+_VALID_URLS.discard("file")
 
 
 fiona = None
@@ -260,10 +262,10 @@ def _read_file(filename, bbox=None, mask=None, rows=None, engine=None, **kwargs)
         # pyogrio/fiona as is (to support downloading only part of the file)
         # otherwise still download manually because pyogrio/fiona don't support
         # all types of urls (https://github.com/geopandas/geopandas/issues/2908)
-        request = urllib.request.urlopen(filename)
-        if not request.getheader("Accept-Ranges") == "bytes":
-            filename = request.read()
-            from_bytes = True
+        with urllib.request.urlopen(filename) as response:
+            if not response.headers.get("Accept-Ranges") == "bytes":
+                filename = response.read()
+                from_bytes = True
 
     if engine == "pyogrio":
         return _read_file_pyogrio(filename, bbox=bbox, mask=mask, rows=rows, **kwargs)


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -577,6 +577,12 @@ def test_read_file_url(engine, url):
     assert isinstance(gdf, geopandas.GeoDataFrame)
 
 
+def test_read_file_local_uri(file_path, engine):
+    local_uri = "file://" + file_path
+    gdf = read_file(local_uri, engine=engine)
+    assert isinstance(gdf, geopandas.GeoDataFrame)
+
+
 def test_read_file_textio(file_path, engine):
     file_text_stream = open(file_path)
     file_stringio = io.StringIO(open(file_path).read())