Repository: Kozea/Radicale

Issue:
Update request (PUT)
When a resource is updated Radicale returns status 201 which is not in compliance with the RFC (https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html). The RFC states that an update of an existing resource SHOULD return either 200 (With extra info) or 204 (preferable)
Most other carddav/caldav servers I have worked with follows this behavior:
A new resource created: 201
An existing resource changed: 204
 


Discussion:
thank you for reporting



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index afbda692a..d128fef57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 * Adjust: [auth] ldap: use ldap_user_attr either first element of list or directly 
 * Fix: use value of property for time range filter
 * Add: [auth] ldap: option ldap_security (none, startls, tls) for additional support of STARTTLS, deprecate ldap_use_ssl
+* Fix: return 204 instead of 201 in case PUT updates an item
 
 ## 3.5.1
 
diff --git a/radicale/app/put.py b/radicale/app/put.py
index fda6140b2..82d04a3f2 100644
--- a/radicale/app/put.py
+++ b/radicale/app/put.py
@@ -282,6 +282,10 @@ def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str,
                         logger.warning(
                             "Bad PUT request on %r (upload): %s", path, e, exc_info=True)
                         return httputils.BAD_REQUEST
+                if (item and item.uid == prepared_item.uid):
+                    logger.debug("PUT request updated existing item %r", path)
+                    headers = {"ETag": etag}
+                    return client.NO_CONTENT, headers, None
 
             headers = {"ETag": etag}
             return client.CREATED, headers, None


Test patch:
diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py
index 75df05927..3639ad2cf 100644
--- a/radicale/tests/test_base.py
+++ b/radicale/tests/test_base.py
@@ -1,7 +1,7 @@
 # This file is part of Radicale - CalDAV and CardDAV server
 # Copyright © 2012-2017 Guillaume Ayoub
 # Copyright © 2017-2022 Unrud <unrud@outlook.com>
-# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
+# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -222,7 +222,7 @@ def test_update_event(self) -> None:
         event_modified = get_file_content("event1_modified.ics")
         path = "/calendar.ics/event1.ics"
         self.put(path, event)
-        self.put(path, event_modified)
+        self.put(path, event_modified, check=204)
         _, answer = self.get("/calendar.ics/")
         assert answer.count("BEGIN:VEVENT") == 1
         _, answer = self.get(path)
@@ -1594,8 +1594,8 @@ def test_report_sync_collection_modify_undo(self) -> None:
         self.put(event_path, event1)
         sync_token, responses = self._report_sync_token(calendar_path)
         assert len(responses) == 1 and responses[event_path] == 200
-        self.put(event_path, event2)
-        self.put(event_path, event1)
+        self.put(event_path, event2, check=204)
+        self.put(event_path, event1, check=204)
         sync_token, responses = self._report_sync_token(
             calendar_path, sync_token)
         if not self.full_sync_token_support and not sync_token: