diff --git a/setup.cfg b/setup.cfg
index 580c0f6..e1e5440 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -50,6 +50,7 @@ tests =
typing_extensions==3.10.0.2
pytest==6.2.5
pytest-cov==2.12.1
+ respx==0.17.1
cheroot==8.5.2
WsgiDAV==3.1.1
colorama==0.4.4
diff --git a/src/webdav4/client.py b/src/webdav4/client.py
index 8651466..4b5fdb3 100644
--- a/src/webdav4/client.py
+++ b/src/webdav4/client.py
@@ -311,7 +311,12 @@ def propfind(
headers=headers,
)
http_resp = self.with_retry(call)
- return parse_multistatus_response(http_resp)
+ msr = parse_multistatus_response(http_resp)
+ if not data and len(msr.responses) == 1:
+ _, response = next(iter(msr.responses.items()))
+ if response.has_propstat and not response.properties.status_ok():
+ raise ResourceNotFound(path)
+ return msr
def get_props(
self,
@@ -547,11 +552,17 @@ def exists(self, path: str) -> bool:
def isdir(self, path: str) -> bool:
"""Checks whether the resource with the given path is a directory."""
- return bool(self.get_props(path).collection)
+ try:
+ return bool(self.get_props(path).collection)
+ except ResourceNotFound:
+ return False
def isfile(self, path: str) -> bool:
"""Checks whether the resource with the given path is a file."""
- return not self.isdir(path)
+ try:
+ return not self.get_props(path).collection
+ except ResourceNotFound:
+ return False
def content_length(self, path: str) -> Optional[int]:
"""Returns content-length of the resource with the given path."""
@@ -586,7 +597,8 @@ def open(
chunk_size: int = None,
) -> Iterator[Union[TextIO, BinaryIO]]:
"""Returns file-like object to a resource."""
- if self.isdir(path):
+ props = self.get_props(path)
+ if props.collection:
raise IsACollectionError(path, "Cannot open a collection")
assert mode in {"r", "rt", "rb"}
diff --git a/src/webdav4/multistatus.py b/src/webdav4/multistatus.py
index d8eddde..0e496a2 100644
--- a/src/webdav4/multistatus.py
+++ b/src/webdav4/multistatus.py
@@ -1,5 +1,6 @@
"""Parsing propfind response."""
+from contextlib import suppress
from http.client import responses
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from xml.etree.ElementTree import Element, ElementTree, SubElement
@@ -88,6 +89,27 @@ def extract_text(prop_name: str) -> Optional[str]:
self.display_name = extract_text("display_name")
+ status_line = (
+ prop(response_xml, "status", relative=True)
+ if response_xml
+ else None
+ )
+ self.status_code: Optional[int] = None
+ if status_line:
+ _, code_str, *_ = status_line.split()
+ with suppress(ValueError):
+ self.status_code = int(code_str)
+
+ self.reason_phrase = (
+ responses[self.status_code] if self.status_code else None
+ )
+
+ def status_ok(self) -> bool:
+ """Check if the propstat:status is ok."""
+ if not self.status_code:
+ return True
+ return 200 <= self.status_code < 300
+
def as_dict(self, raw: bool = False) -> Dict[str, Any]:
"""Returns all properties that it supports parsing.
@@ -144,7 +166,8 @@ def __init__(self, response_xml: Element) -> None:
code = None
if status_line:
_, code_str, *_ = status_line.split()
- code = int(code_str)
+ with suppress(ValueError):
+ code = int(code_str)
self.status_code = code
self.reason_phrase = (
diff --git a/tests/test_client.py b/tests/test_client.py
index 0a4fbc4..2a1f0a5 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,4 +1,5 @@
"""Tests for webdav client."""
+import textwrap
from datetime import datetime, timezone
from http import HTTPStatus
from io import DEFAULT_BUFFER_SIZE, BytesIO
@@ -8,6 +9,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from respx import MockRouter
from webdav4.client import (
BadGatewayError,
@@ -792,23 +794,8 @@ def test_isdir_isfile(storage_dir: TmpDir, client: Client):
assert client.isfile("/data/foo")
assert not client.isdir("/data/foo")
- with pytest.raises(ResourceNotFound) as exc_info:
- client.isdir("/data/file")
-
- assert exc_info.value.path == "/data/file"
- assert (
- str(exc_info.value)
- == "The resource /data/file could not be found in the server"
- )
-
- with pytest.raises(ResourceNotFound) as exc_info:
- client.isdir("/data/file")
-
- assert exc_info.value.path == "/data/file"
- assert (
- str(exc_info.value)
- == "The resource /data/file could not be found in the server"
- )
+ assert not client.isfile("/data/file")
+ assert not client.isdir("/data/file")
def test_exists(storage_dir: TmpDir, client: Client):
@@ -969,3 +956,43 @@ def test_client_retries(client: Client, server_address: URL):
)
client.copy("/container1", "/container2")
assert func.call_count == 3
+
+
+def test_handle_nginx_propfind_responses_correctly(respx_mock: MockRouter):
+ """Add support for Propfind response from nginx."""
+ from httpx import Response
+
+ path = "not-existing-file"
+ content = textwrap.dedent(
+ f"""\
+
+
+
+ /{path}
+
+
+ HTTP/1.1 404 Not Found
+
+
+ """
+ )
+ base_url = "https://example.org"
+ client = Client(base_url)
+ route = respx_mock.request(Method.PROPFIND, f"{base_url}/{path}").mock(
+ return_value=Response(HTTPStatus.MULTI_STATUS, content=content)
+ )
+
+ assert not client.exists(path)
+ with pytest.raises(ResourceNotFound):
+ client.propfind(path)
+
+ assert not client.isdir(path)
+ assert not client.isfile(path)
+
+ with pytest.raises(ResourceNotFound):
+ client.info(path)
+
+ with pytest.raises(ResourceNotFound):
+ assert client.ls(path)
+
+ assert route.called
diff --git a/tests/test_multistatus.py b/tests/test_multistatus.py
index 00cf2db..f5d72df 100644
--- a/tests/test_multistatus.py
+++ b/tests/test_multistatus.py
@@ -71,6 +71,9 @@ def test_dav_properties_empty(args: Tuple[Element]):
assert props.display_name is None
assert props.collection is None
assert props.resource_type is None
+ assert props.status_code is None
+ assert props.reason_phrase is None
+ assert props.status_ok()
def test_dav_properties():
@@ -88,6 +91,7 @@ def test_dav_properties():
"8db748065bfed5c0731e9c7ee5f9bf4c"
text/plain
+ HTTP/1.1 200 OK
"""
elem = fromstring(content)
@@ -116,6 +120,8 @@ def test_dav_properties():
assert props.display_name == "foo"
assert props.collection is False
assert props.resource_type == "file"
+ assert props.status_code == 200
+ assert props.reason_phrase == "OK"
assert props.as_dict() == {
"created": datetime(2020, 1, 2, 3, 4, 5, tzinfo=tzutc()),
@@ -127,6 +133,7 @@ def test_dav_properties():
"display_name": "foo",
"type": "file",
}
+ assert props.status_ok()
def test_dav_properties_partial():
@@ -166,6 +173,10 @@ def test_dav_properties_partial():
assert props.collection is True
assert props.resource_type == "directory"
+ assert props.status_code is None
+ assert props.reason_phrase is None
+ assert props.status_ok()
+
@pytest.mark.parametrize(
"href, absolute",