Repository navigation
SFTP CreateDirectoryAsync is not idempotent #1747
Copy link
Copy link
Open
Description
My specifications:
OS: Ubuntu Desktop and Windows 11
Framework: .NET 9
Package version: 2025.1.0
This is my current code:
using SftpClient client = new SftpClient(host, port, username, password);
await client.ConnectAsync(cancellationToken);
try
{
await client.CreateDirectoryAsync(someDir, cancellationToken);
}
catch (SftpException ex)
{
// Only some logging code here.
}When this directory already exists, all I get as an exception message is "failure".
Renci.SshNet.Common.SftpException: failure
at Renci.SshNet.SubsystemSession.<WaitOnHandleAsync> g__DoWaitAsync | 38_0[T](TaskCompletionSource`1 tcs, Int32 millisecondsTimeout, CancellationToken cancellationToken)
at Renci.SshNet.SftpClient.CreateDirectoryAsync(String path, CancellationToken cancellationToken)
Problems:
- I can't know for sure if the exception is about the directory already existing, or whether something else is wrong. Type SftpException with message "failure" is just not descriptive enough.
- CreateDirectoryAsync is not idempotent. Throwing an exception seems very counter-intuitive for a method like this.
- The SftpException is not described in the method documentation:
/// <summary>
/// Asynchronously requests to create a remote directory specified by path.
/// </summary>
/// <param name="path">Directory path to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous create directory operation.</returns>
/// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="SftpPermissionDeniedException">Permission to create the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>If I would want to do this properly, I need quite a lot of code. This is the extension method I wrote:
internal static async ValueTask EnsureDirectoryExists(this SftpClient client, string directory, CancellationToken cancellationToken)
{
bool directoryExists = await client.ExistsAsync(directory, cancellationToken);
if (directoryExists) return;
try
{
await client.CreateDirectoryAsync(directory, cancellationToken);
}
catch (SftpException ex)
{
try
{
bool directoryAlreadyExisted= await client.ExistsAsync(directory, cancellationToken);
if (directoryAlreadyExisted)
{
// The directory already existed, so we can ignore the exception.
return;
}
else
{
// The directory does not exist.
throw;
}
}
catch (SftpException)
{
// Failed checking if the directory exists.
// Throwing the original exception as that illustrates what really is the problem here.
throw ex;
}
}
}There are 2 ways this could be fixed:
- Add an EnsureDirectoryExists method. I'm sure this can be done much closer to the actual SFTP protocol than the extension method I created.
- Add an SftpDirectoryAlreadyExistsException that inherits from SftpException.
I'm very curious, is there a reason why something like EnsureDirectoryExists was never added to SSH.NET?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
JPasterkampRotec commentedon Dec 11, 2025
I've looked a bit deeper into the SFTP protocol and from what I can see an SFTP server will always return one of these status codes:
I assume creating a directory that already exists will always return SSH_FX_FAILURE (4), which is the reason this issue was never addressed.
mus65 commentedon Dec 22, 2025
As you already noticed, the protocol simply doesn't give a more descriptive error, so there is nothing SSH.NET can do about it.
Since SSH.NET can't detect this specific failure, I don't see what else SSH.NET could do. Always checking if the directory exists before is imho not correct, since it introduces another roundtrip to the server.
yeah, that is probably worth fixing (SftpException inherits from SshException though, so the documentation is not completely wrong).
Isn't the whole
catchblock redundant, since you already checkedExistsAsyncpreviously?I get that this seems counter-intutive, but since the protocol doesn't have a proper error code for this, I don't see what SSH.NET could do. I'm not sure a
EnsureDirectoryExistswould be accepted either since it would be nothing more than doing an exists check beforehand (I'm not a maintainer though).JPasterkampRotec commentedon Dec 22, 2025
It prevents race conditions like:
Yes, I totally understand there is no pretty fix for this, because the protocol does not support it.
0liver commentedon Aug 13, 2026
@WojciechNagorski Wdyt? Would this library accept a convenience method like the one mentioned above?