Skip to content

Commit aa35c67

Browse files
authored
Merge pull request #1695 from pbiering/issue-1693-fix-http-return-codes
Issue 1693 fix http return codes
2 parents d79abc2 + 19a4715 commit aa35c67

File tree

8 files changed

+129
-42
lines changed

8 files changed

+129
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 3.4.2.dev
44

55
* Add: option [auth] type oauth2 by code migration from https://gitlab.mim-libre.fr/alphabet/radicale_oauth/-/blob/dev/oauth2/
6+
* Fix: catch OS errors on PUT MKCOL MKCALENDAR MOVE PROPPATCH (insufficient storage, access denied, internal server error)
67

78
## 3.4.1
89
* Add: option [auth] dovecot_connection_type / dovecot_host / dovecot_port

radicale/app/mkcalendar.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# Copyright © 2008 Nicolas Kandel
33
# Copyright © 2008 Pascal Halter
44
# Copyright © 2008-2017 Guillaume Ayoub
5-
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
5+
# Copyright © 2017-2021 Unrud <unrud@outlook.com>
6+
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
67
#
78
# This library is free software: you can redistribute it and/or modify
89
# it under the terms of the GNU General Public License as published by
@@ -17,7 +18,9 @@
1718
# You should have received a copy of the GNU General Public License
1819
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
1920

21+
import errno
2022
import posixpath
23+
import re
2124
import socket
2225
from http import client
2326

@@ -70,7 +73,20 @@ def do_MKCALENDAR(self, environ: types.WSGIEnviron, base_prefix: str,
7073
try:
7174
self._storage.create_collection(path, props=props)
7275
except ValueError as e:
73-
logger.warning(
74-
"Bad MKCALENDAR request on %r: %s", path, e, exc_info=True)
75-
return httputils.BAD_REQUEST
76+
# return better matching HTTP result in case errno is provided and catched
77+
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
78+
if errno_match:
79+
logger.error(
80+
"Failed MKCALENDAR request on %r: %s", path, e, exc_info=True)
81+
errno_e = int(errno_match.group(1))
82+
if errno_e == errno.ENOSPC:
83+
return httputils.INSUFFICIENT_STORAGE
84+
elif errno_e in [errno.EPERM, errno.EACCES]:
85+
return httputils.FORBIDDEN
86+
else:
87+
return httputils.INTERNAL_SERVER_ERROR
88+
else:
89+
logger.warning(
90+
"Bad MKCALENDAR request on %r: %s", path, e, exc_info=True)
91+
return httputils.BAD_REQUEST
7692
return client.CREATED, {}, None

radicale/app/mkcol.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# Copyright © 2008 Nicolas Kandel
33
# Copyright © 2008 Pascal Halter
44
# Copyright © 2008-2017 Guillaume Ayoub
5-
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
5+
# Copyright © 2017-2021 Unrud <unrud@outlook.com>
6+
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
67
#
78
# This library is free software: you can redistribute it and/or modify
89
# it under the terms of the GNU General Public License as published by
@@ -17,7 +18,9 @@
1718
# You should have received a copy of the GNU General Public License
1819
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
1920

21+
import errno
2022
import posixpath
23+
import re
2124
import socket
2225
from http import client
2326

@@ -74,8 +77,21 @@ def do_MKCOL(self, environ: types.WSGIEnviron, base_prefix: str,
7477
try:
7578
self._storage.create_collection(path, props=props)
7679
except ValueError as e:
77-
logger.warning(
78-
"Bad MKCOL request on %r (type:%s): %s", path, collection_type, e, exc_info=True)
79-
return httputils.BAD_REQUEST
80+
# return better matching HTTP result in case errno is provided and catched
81+
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
82+
if errno_match:
83+
logger.error(
84+
"Failed MKCOL request on %r (type:%s): %s", path, collection_type, e, exc_info=True)
85+
errno_e = int(errno_match.group(1))
86+
if errno_e == errno.ENOSPC:
87+
return httputils.INSUFFICIENT_STORAGE
88+
elif errno_e in [errno.EPERM, errno.EACCES]:
89+
return httputils.FORBIDDEN
90+
else:
91+
return httputils.INTERNAL_SERVER_ERROR
92+
else:
93+
logger.warning(
94+
"Bad MKCOL request on %r (type:%s): %s", path, collection_type, e, exc_info=True)
95+
return httputils.BAD_REQUEST
8096
logger.info("MKCOL request %r (type:%s): %s", path, collection_type, "successful")
8197
return client.CREATED, {}, None

radicale/app/move.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# Copyright © 2008 Nicolas Kandel
33
# Copyright © 2008 Pascal Halter
44
# Copyright © 2008-2017 Guillaume Ayoub
5-
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
5+
# Copyright © 2017-2023 Unrud <unrud@outlook.com>
6+
# Copyright © 2023-2025 Peter Bieringer <pb@bieringer.de>
67
#
78
# This library is free software: you can redistribute it and/or modify
89
# it under the terms of the GNU General Public License as published by
@@ -17,6 +18,7 @@
1718
# You should have received a copy of the GNU General Public License
1819
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
1920

21+
import errno
2022
import posixpath
2123
import re
2224
from http import client
@@ -109,7 +111,20 @@ def do_MOVE(self, environ: types.WSGIEnviron, base_prefix: str,
109111
try:
110112
self._storage.move(item, to_collection, to_href)
111113
except ValueError as e:
112-
logger.warning(
113-
"Bad MOVE request on %r: %s", path, e, exc_info=True)
114-
return httputils.BAD_REQUEST
114+
# return better matching HTTP result in case errno is provided and catched
115+
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
116+
if errno_match:
117+
logger.error(
118+
"Failed MOVE request on %r: %s", path, e, exc_info=True)
119+
errno_e = int(errno_match.group(1))
120+
if errno_e == errno.ENOSPC:
121+
return httputils.INSUFFICIENT_STORAGE
122+
elif errno_e in [errno.EPERM, errno.EACCES]:
123+
return httputils.FORBIDDEN
124+
else:
125+
return httputils.INTERNAL_SERVER_ERROR
126+
else:
127+
logger.warning(
128+
"Bad MOVE request on %r: %s", path, e, exc_info=True)
129+
return httputils.BAD_REQUEST
115130
return client.NO_CONTENT if to_item else client.CREATED, {}, None

radicale/app/proppatch.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
# Copyright © 2008 Nicolas Kandel
33
# Copyright © 2008 Pascal Halter
44
# Copyright © 2008-2017 Guillaume Ayoub
5-
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
5+
# Copyright © 2017-2020 Unrud <unrud@outlook.com>
6+
# Copyright © 2020-2020 Tuna Celik <tuna@jakpark.com>
7+
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
68
#
79
# This library is free software: you can redistribute it and/or modify
810
# it under the terms of the GNU General Public License as published by
@@ -17,6 +19,8 @@
1719
# You should have received a copy of the GNU General Public License
1820
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
1921

22+
import errno
23+
import re
2024
import socket
2125
import xml.etree.ElementTree as ET
2226
from http import client
@@ -107,7 +111,20 @@ def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str,
107111
)
108112
self._hook.notify(hook_notification_item)
109113
except ValueError as e:
110-
logger.warning(
111-
"Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
112-
return httputils.BAD_REQUEST
114+
# return better matching HTTP result in case errno is provided and catched
115+
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
116+
if errno_match:
117+
logger.error(
118+
"Failed PROPPATCH request on %r: %s", path, e, exc_info=True)
119+
errno_e = int(errno_match.group(1))
120+
if errno_e == errno.ENOSPC:
121+
return httputils.INSUFFICIENT_STORAGE
122+
elif errno_e in [errno.EPERM, errno.EACCES]:
123+
return httputils.FORBIDDEN
124+
else:
125+
return httputils.INTERNAL_SERVER_ERROR
126+
else:
127+
logger.warning(
128+
"Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
129+
return httputils.BAD_REQUEST
113130
return client.MULTI_STATUS, headers, self._xml_response(xml_answer)

radicale/app/put.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Copyright © 2008-2017 Guillaume Ayoub
55
# Copyright © 2017-2020 Unrud <unrud@outlook.com>
66
# Copyright © 2020-2023 Tuna Celik <tuna@jakpark.com>
7-
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
7+
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
88
#
99
# This library is free software: you can redistribute it and/or modify
1010
# it under the terms of the GNU General Public License as published by
@@ -19,8 +19,10 @@
1919
# You should have received a copy of the GNU General Public License
2020
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
2121

22+
import errno
2223
import itertools
2324
import posixpath
25+
import re
2426
import socket
2527
import sys
2628
from http import client
@@ -264,9 +266,22 @@ def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str,
264266
)
265267
self._hook.notify(hook_notification_item)
266268
except ValueError as e:
267-
logger.warning(
268-
"Bad PUT request on %r (upload): %s", path, e, exc_info=True)
269-
return httputils.BAD_REQUEST
269+
# return better matching HTTP result in case errno is provided and catched
270+
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
271+
if errno_match:
272+
logger.error(
273+
"Failed PUT request on %r (upload): %s", path, e, exc_info=True)
274+
errno_e = int(errno_match.group(1))
275+
if errno_e == errno.ENOSPC:
276+
return httputils.INSUFFICIENT_STORAGE
277+
elif errno_e in [errno.EPERM, errno.EACCES]:
278+
return httputils.FORBIDDEN
279+
else:
280+
return httputils.INTERNAL_SERVER_ERROR
281+
else:
282+
logger.warning(
283+
"Bad PUT request on %r (upload): %s", path, e, exc_info=True)
284+
return httputils.BAD_REQUEST
270285

271286
headers = {"ETag": etag}
272287
return client.CREATED, headers, None

radicale/httputils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Copyright © 2008 Pascal Halter
44
# Copyright © 2008-2017 Guillaume Ayoub
55
# Copyright © 2017-2022 Unrud <unrud@outlook.com>
6-
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
6+
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
77
#
88
# This library is free software: you can redistribute it and/or modify
99
# it under the terms of the GNU General Public License as published by
@@ -79,6 +79,9 @@
7979
DIRECTORY_LISTING: types.WSGIResponse = (
8080
client.FORBIDDEN, (("Content-Type", "text/plain"),),
8181
"Directory listings are not supported.")
82+
INSUFFICIENT_STORAGE: types.WSGIResponse = (
83+
client.INSUFFICIENT_STORAGE, (("Content-Type", "text/plain"),),
84+
"Insufficient Storage. Please contact the administrator.")
8285
INTERNAL_SERVER_ERROR: types.WSGIResponse = (
8386
client.INTERNAL_SERVER_ERROR, (("Content-Type", "text/plain"),),
8487
"A server error occurred. Please contact the administrator.")

radicale/storage/multifilesystem/create_collection.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright © 2014 Jean-Marc Martins
33
# Copyright © 2012-2017 Guillaume Ayoub
44
# Copyright © 2017-2021 Unrud <unrud@outlook.com>
5-
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
5+
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
66
#
77
# This library is free software: you can redistribute it and/or modify
88
# it under the terms of the GNU General Public License as published by
@@ -50,27 +50,31 @@ def create_collection(self, href: str,
5050
self._makedirs_synced(parent_dir)
5151

5252
# Create a temporary directory with an unsafe name
53-
with TemporaryDirectory(prefix=".Radicale.tmp-", dir=parent_dir
54-
) as tmp_dir:
55-
# The temporary directory itself can't be renamed
56-
tmp_filesystem_path = os.path.join(tmp_dir, "collection")
57-
os.makedirs(tmp_filesystem_path)
58-
col = self._collection_class(
59-
cast(multifilesystem.Storage, self),
60-
pathutils.unstrip_path(sane_path, True),
61-
filesystem_path=tmp_filesystem_path)
62-
col.set_meta(props)
63-
if items is not None:
64-
if props.get("tag") == "VCALENDAR":
65-
col._upload_all_nonatomic(items, suffix=".ics")
66-
elif props.get("tag") == "VADDRESSBOOK":
67-
col._upload_all_nonatomic(items, suffix=".vcf")
53+
try:
54+
with TemporaryDirectory(prefix=".Radicale.tmp-", dir=parent_dir
55+
) as tmp_dir:
56+
# The temporary directory itself can't be renamed
57+
tmp_filesystem_path = os.path.join(tmp_dir, "collection")
58+
os.makedirs(tmp_filesystem_path)
59+
col = self._collection_class(
60+
cast(multifilesystem.Storage, self),
61+
pathutils.unstrip_path(sane_path, True),
62+
filesystem_path=tmp_filesystem_path)
63+
col.set_meta(props)
64+
if items is not None:
65+
if props.get("tag") == "VCALENDAR":
66+
col._upload_all_nonatomic(items, suffix=".ics")
67+
elif props.get("tag") == "VADDRESSBOOK":
68+
col._upload_all_nonatomic(items, suffix=".vcf")
6869

69-
if os.path.lexists(filesystem_path):
70-
pathutils.rename_exchange(tmp_filesystem_path, filesystem_path)
71-
else:
72-
os.rename(tmp_filesystem_path, filesystem_path)
73-
self._sync_directory(parent_dir)
70+
if os.path.lexists(filesystem_path):
71+
pathutils.rename_exchange(tmp_filesystem_path, filesystem_path)
72+
else:
73+
os.rename(tmp_filesystem_path, filesystem_path)
74+
self._sync_directory(parent_dir)
75+
except Exception as e:
76+
raise ValueError("Failed to create collection %r as %r %s" %
77+
(href, filesystem_path, e)) from e
7478

7579
return self._collection_class(
7680
cast(multifilesystem.Storage, self),

0 commit comments

Comments
 (0)