diff --git a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs index 4982231..84d1209 100644 --- a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs +++ b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs @@ -99,7 +99,7 @@ namespace PSProxmoxVE.Core.Client { var request = BuildRequest(HttpMethod.Post, resource, mutating: true); if (data != null) - request.Content = new FormUrlEncodedContent(data); + request.Content = BuildFormContent(data); return await SendAsync(request, resource, "POST").ConfigureAwait(false); } @@ -111,7 +111,7 @@ namespace PSProxmoxVE.Core.Client { var request = BuildRequest(HttpMethod.Put, resource, mutating: true); if (data != null) - request.Content = new FormUrlEncodedContent(data); + request.Content = BuildFormContent(data); return await SendAsync(request, resource, "PUT").ConfigureAwait(false); } @@ -124,6 +124,52 @@ namespace PSProxmoxVE.Core.Client return await SendAsync(request, resource, "DELETE").ConfigureAwait(false); } + // ------------------------------------------------------------------------- + // Form body encoding + // ------------------------------------------------------------------------- + + /// + /// Builds form-encoded content using minimal encoding that matches curl behavior. + /// .NET's over-encodes characters like + /// : and ! which PVE's internal API consumers (e.g. cluster join) + /// do not properly URL-decode. + /// + private static StringContent BuildFormContent(Dictionary data) + { + var sb = new StringBuilder(); + foreach (var kvp in data) + { + if (sb.Length > 0) sb.Append('&'); + sb.Append(EncodeFormValue(kvp.Key)); + sb.Append('='); + sb.Append(EncodeFormValue(kvp.Value)); + } + return new StringContent(sb.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded"); + } + + /// + /// Encodes a form value with minimal percent-encoding — only characters that + /// would break form parsing are encoded. This matches curl's -d behavior + /// and avoids over-encoding that PVE does not handle correctly. + /// + private static string EncodeFormValue(string value) + { + var sb = new StringBuilder(value.Length); + foreach (char c in value) + { + switch (c) + { + case '&': sb.Append("%26"); break; + case '=': sb.Append("%3D"); break; + case '+': sb.Append("%2B"); break; + case ' ': sb.Append('+'); break; + case '%': sb.Append("%25"); break; + default: sb.Append(c); break; + } + } + return sb.ToString(); + } + // ------------------------------------------------------------------------- // Synchronous wrappers // -------------------------------------------------------------------------