Summary
When a server responds 401 without a WWW-Authenticate header, DAVClient never
builds an auth object and never transmits the supplied credentials. The bare 401
propagates to the caller as AuthorizationError, which is indistinguishable from a
genuinely rejected password — but the password was never sent.
Yahoo Calendar's CalDAV server (caldav.calendar.yahoo.com) behaves this way. RFC 7235
§3.1 requires WWW-Authenticate on a 401, so this is a server bug, but it is a server
bug that currently makes Yahoo unreachable through this library even with valid
credentials.
Affected versions
Reproduced on 2.1.0 and 2.2.6.
Reproduction — no account or credentials required
The deliberately wrong password is the point: it demonstrates that self.auth is still
None after the request, meaning nothing was ever transmitted.
import caldav
client = caldav.DAVClient(
url="https://caldav.calendar.yahoo.com/",
username="anyone@yahoo.com",
password="does-not-matter",
)
try:
client.principal()
except Exception as exc:
print(type(exc).__name__) # AuthorizationError
print(client.auth) # None <- credential never sent
On 2.1.0 this additionally logs a CRITICAL with an lxml.etree.XMLSyntaxError
traceback, because Yahoo's error body is JSON where XML is expected:
CRITICAL:root:Expected some valid XML from the server, but got this:
b'{"error":{"code": 4003, "message":"Invalid User credential"}}'
The exception is caught and AuthorizationError is raised as above, so this is noise
rather than a second failure — but it is alarming in downstream logs and would disappear
along with the root cause. 2.2.6 does not log it.
With auth_type="basic" the auth object is built up front and the request succeeds
(verified against a real Yahoo account: 207, principal /principals/users/<user>/,
and all calendars enumerate normally).
Server behaviour
$ curl -s -D - -o /dev/null -X PROPFIND https://caldav.calendar.yahoo.com/ -H 'Depth: 0'
HTTP/2 401
content-type: application/json
server: ATS
No WWW-Authenticate header in the response. The server is otherwise a functioning
CalDAV endpoint:
$ curl -s -D - -o /dev/null -X OPTIONS https://caldav.calendar.yahoo.com/
HTTP/2 200
dav: 1, 3, calendar-access
allow: HEAD, MKCOL, POST, PROPFIND, ACL, COPY, REPORT, OPTIONS, PUT, DELETE, MKCALENDAR, MOVE, GET, PROPPATCH
Cause
davclient.py (2.1.0, ~line 950) gates the retry-with-credentials path on the header
being present:
if (
r.status_code == 401
and "WWW-Authenticate" in r_headers
and not self.auth
and (self.username or self.password)
):
auth_types = self.extract_auth_types(r_headers["WWW-Authenticate"])
self.build_auth_object(auth_types)
...
return self.request(url, method, body, headers)
When the header is absent the branch is skipped entirely, so build_auth_object() is
never reached and the 401 is returned as-is.
Proposed fix
When a 401 carries no WWW-Authenticate, and credentials were supplied, and no auth
object exists yet, attempt basic once rather than giving up:
elif (
r.status_code == 401
and "WWW-Authenticate" not in r_headers
and not self.auth
and self.username
and self.password
):
# RFC 7235 requires WWW-Authenticate on a 401, but some servers (Yahoo
# Calendar) omit it. Without a declared scheme we cannot negotiate, so
# try basic once before surfacing the 401.
self.auth_type = "basic"
self.build_auth_object()
return self.request(url, method, body, headers)
Guarding on a retry flag would prevent a loop against a server that returns 401 to the
authenticated request as well.
Note on preemptive credentials
This does send Basic credentials to a server that never declared support for them. That
is already what auth_type="basic" does today, it only happens after an explicit 401,
and the connection is TLS. If sending unprompted credentials is considered too
aggressive as a default, an opt-in flag (allow_unprompted_basic=True) would still be a
large improvement over the current behaviour, because the failure today is silent and
misattributed.
Why this is worth fixing rather than documenting
Downstream consumers surface the resulting exception as a rejected credential. Home
Assistant's CalDAV integration reports "invalid authentication", which sends users to
regenerate app passwords, retype them, and suspect their clipboard — none of which can
work, because the credential is never transmitted. The library holds the only evidence
that distinguishes the two cases.
Workaround for anyone who lands here
caldav.DAVClient(url=..., username=..., password=..., auth_type="basic")
Home Assistant's config flow does not expose auth_type, so HA users additionally need a
small custom component wrapping DAVClient.__init__ to inject it.
Summary
When a server responds
401without aWWW-Authenticateheader,DAVClientneverbuilds an auth object and never transmits the supplied credentials. The bare 401
propagates to the caller as
AuthorizationError, which is indistinguishable from agenuinely rejected password — but the password was never sent.
Yahoo Calendar's CalDAV server (
caldav.calendar.yahoo.com) behaves this way. RFC 7235§3.1 requires
WWW-Authenticateon a 401, so this is a server bug, but it is a serverbug that currently makes Yahoo unreachable through this library even with valid
credentials.
Affected versions
Reproduced on 2.1.0 and 2.2.6.
Reproduction — no account or credentials required
The deliberately wrong password is the point: it demonstrates that
self.authis stillNoneafter the request, meaning nothing was ever transmitted.On 2.1.0 this additionally logs a
CRITICALwith anlxml.etree.XMLSyntaxErrortraceback, because Yahoo's error body is JSON where XML is expected:
The exception is caught and
AuthorizationErroris raised as above, so this is noiserather than a second failure — but it is alarming in downstream logs and would disappear
along with the root cause. 2.2.6 does not log it.
With
auth_type="basic"the auth object is built up front and the request succeeds(verified against a real Yahoo account:
207, principal/principals/users/<user>/,and all calendars enumerate normally).
Server behaviour
No
WWW-Authenticateheader in the response. The server is otherwise a functioningCalDAV endpoint:
Cause
davclient.py(2.1.0, ~line 950) gates the retry-with-credentials path on the headerbeing present:
When the header is absent the branch is skipped entirely, so
build_auth_object()isnever reached and the 401 is returned as-is.
Proposed fix
When a 401 carries no
WWW-Authenticate, and credentials were supplied, and no authobject exists yet, attempt
basiconce rather than giving up:Guarding on a retry flag would prevent a loop against a server that returns 401 to the
authenticated request as well.
Note on preemptive credentials
This does send Basic credentials to a server that never declared support for them. That
is already what
auth_type="basic"does today, it only happens after an explicit 401,and the connection is TLS. If sending unprompted credentials is considered too
aggressive as a default, an opt-in flag (
allow_unprompted_basic=True) would still be alarge improvement over the current behaviour, because the failure today is silent and
misattributed.
Why this is worth fixing rather than documenting
Downstream consumers surface the resulting exception as a rejected credential. Home
Assistant's CalDAV integration reports "invalid authentication", which sends users to
regenerate app passwords, retype them, and suspect their clipboard — none of which can
work, because the credential is never transmitted. The library holds the only evidence
that distinguishes the two cases.
Workaround for anyone who lands here
Home Assistant's config flow does not expose
auth_type, so HA users additionally need asmall custom component wrapping
DAVClient.__init__to inject it.