WebDAV client library, written in Rust.
This library is composed of 3 feature-gated layers:
- Low-level I/O-free coroutines: these
no_std-compatible state machines contain the whole WebDAV logic and can be used anywhere - Mid-level light client: a standard, blocking WebDAV client using a
Stream: Read + Write - High-level full client: light client + TCP connections and TLS negotiations handled for you
- I/O-free coroutines:
no_stdstate machines; no sockets, no async runtime, nostdrequired, drive against any blocking, async, or fuzz harness. - Light standard, blocking client (requires
clientfeature) - Full standard, blocking client with TLS support:
- Rustls with ring crypto (requires
rustls-ringfeature) - Rustls with aws crypto (requires
rustls-awsfeature) - Native TLS (requires
native-tlsfeature)
- Rustls with ring crypto (requires
- CalDAV calendars and items
- CardDAV addressbooks and cards
- HTTP Auth mechanisms:
BASIC,BEARER
Tip
I/O WebDAV is written in Rust and uses cargo features to gate backend support. The default feature set is declared in Cargo.toml or on docs.rs.
| Module | What it covers |
|---|---|
| 4918 | WebDAV core: PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, DELETE, GET, PUT, OPTIONS, multistatus parsing |
| 4791 | CalDAV: calendar collections and calendar object resources (items) |
| 5397 | WebDAV current principal: current-user-principal discovery |
| 6352 | CardDAV: addressbook collections and address object resources (cards) |
| 6764 | Service discovery: .well-known/caldav and .well-known/carddav bootstrap |
I/O WebDAV can be consumed three ways, depending on how much of the I/O stack you want to own. Each mode is gated by cargo features.
Whichever mode you pick, every standard-shape coroutine implements the WebdavCoroutine trait with two associated types: Yield (intermediate progress) and Return (terminal value, by convention Result<Output, Error>). Its resume(arg: Option<&[u8]>) method returns a WebdavCoroutineState<Yield, Return> with two variants:
Yielded(Yield): intermediate yield. Most coroutines pick the standardWebdavYieldwithWantsRead/WantsWrite(Vec<u8>). PassSome(&[])afterWantsReadto signal EOF.Complete(Return): terminal yield, carryingOk(Output)on success orErr(Error)on failure.
The redirect-capable discovery coroutines (CurrentUserPrincipal, CalendarHomeSet, AddressbookHomeSet, FollowRedirects) declare their own WebdavRedirectYield which extends the standard variants with WantsRedirect { url, keep_alive, same_origin }: the server responded with a 3xx and the caller chooses whether to open a new connection to url and retry, or surface the redirect as an error.
No features required: works in #![no_std], no sockets, no async runtime. You own the loop and the bytes; the library only produces request bytes and consumes server responses.
Discover the current user principal against a blocking rustls socket:
use std::{io::{Read, Write}, net::TcpStream, sync::Arc};
use io_webdav::{
coroutine::{WebdavCoroutine, WebdavCoroutineState},
rfc4918::{WebdavAuth, coroutine::WebdavRedirectYield},
rfc5397::current_user_principal::CurrentUserPrincipal,
};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;
use io_http::rfc7617::basic::HttpAuthBasic;
use url::Url;
let base_url = Url::parse("https://dav.example.org/").unwrap();
let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));
let config = ClientConfig::with_platform_verifier().unwrap();
let server_name = base_url.host_str().unwrap().to_string().try_into().unwrap();
let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
let tcp = TcpStream::connect((base_url.host_str().unwrap(), 443)).unwrap();
let mut stream = StreamOwned::new(conn, tcp);
let mut coroutine = CurrentUserPrincipal::new(&base_url, &auth, "io-webdav");
let mut arg: Option<&[u8]> = None;
let mut buf = [0u8; 8192];
let mut read_buf = Vec::<u8>::new();
let principal = loop {
match coroutine.resume(arg.take()) {
WebdavCoroutineState::Complete(Ok(principal)) => break principal,
WebdavCoroutineState::Complete(Err(err)) => panic!("{err}"),
WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRead) => {
let n = stream.read(&mut buf).unwrap();
read_buf.clear();
read_buf.extend_from_slice(&buf[..n]);
arg = Some(&read_buf);
}
WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).unwrap();
}
WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRedirect { url, .. }) => {
todo!("reconnect to {url}");
}
}
};
println!("Principal: {principal:?}");Enable the client feature. WebdavClientStd::new(stream, auth, base_url) wraps any blocking Read + Write and exposes one method per WebDAV operation, plus the cached discovery flow (current_user_principal → calendar_home_set / addressbook_home_set). You still open the TCP socket and run TLS yourself, and hand over a ready-to-talk stream; the client takes it from there.
[dependencies]
io-webdav = { version = "0.0.1", default-features = false, features = ["client"] }use std::{net::TcpStream, sync::Arc};
use io_webdav::{client::WebdavClientStd, rfc4918::WebdavAuth};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;
use io_http::rfc7617::basic::HttpAuthBasic;
use url::Url;
let base_url = Url::parse("https://dav.example.org/").unwrap();
let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));
let config = ClientConfig::with_platform_verifier().unwrap();
let server_name = base_url.host_str().unwrap().to_string().try_into().unwrap();
let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
let tcp = TcpStream::connect((base_url.host_str().unwrap(), 443)).unwrap();
let stream = StreamOwned::new(conn, tcp);
let mut client = WebdavClientStd::new(stream, auth, base_url);
client.calendar_home_set().unwrap();
for calendar in client.list_calendars().unwrap() {
println!("{}: {:?}", calendar.id, calendar.display_name);
}Enable one of the TLS feature flags: rustls-ring (default), rustls-aws, or native-tls. WebdavClientStd::connect(url, tls, auth) opens http:// / https:// URLs via pimalaya/stream.
[dependencies]
io-webdav = "0.0.1" # rustls-ring is enabled by defaultuse io_webdav::{client::WebdavClientStd, rfc4918::WebdavAuth};
use pimalaya_stream::tls::Tls;
use io_http::rfc7617::basic::HttpAuthBasic;
use url::Url;
let base_url = Url::parse("https://dav.example.org/").unwrap();
let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));
let tls = Tls::default();
let mut client = WebdavClientStd::connect(&base_url, &tls, auth).unwrap();
client.calendar_home_set().unwrap();
for calendar in client.list_calendars().unwrap() {
println!("{}: {:?}", calendar.id, calendar.display_name);
}When discovery surfaces a different authority than where you first connected (an RFC 6764 .well-known redirect, a cross-origin home-set), use WebdavClientStd::set_stream to swap in a new transport.
This project is developed with AI assistance. This section documents how, so users and downstream packagers can make informed decisions.
-
Tools: Claude Code (Anthropic), Opus 4.8, invoked locally with a persistent project-scoped memory and a small set of repo-specific rules.
-
Used for: Refactors, mechanical multi-file edits, boilerplate (feature gates, error enums, derive macros, trait impls), test scaffolding, doc polish, exploratory design conversations.
-
Not used for: Engineering, critical code, git manipulation (commit, merge, rebase…), real-world tests.
-
Verification: Every AI-assisted change is read, compiled, tested, and formatted before commit (
nix develop --command cargo check / cargo test / cargo fmt). Behavioural correctness is verified against the relevant RFC or upstream spec, not assumed from the model output. Tests are never adjusted to fit AI-generated code; the code is adjusted to fit correct behaviour. -
Limitations: AI models occasionally produce code that compiles and passes tests but is subtly wrong: off-by-one errors, missed edge cases, plausible but nonexistent APIs, stale RFC references. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously.
-
Last reviewed: 10/06/2026
This project is licensed under either of:
at your option.
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
- 2022 → 2023: NGI Assure
- 2023 → 2024: NGI Zero Entrust
- 2024 → 2026: NGI Zero Core
- 2027 in preparation…
If you appreciate the project, feel free to donate using one of the following providers:
