From c7f30eea434e1ed4eb3581cd745130958cec8cca Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 14 Jun 2026 19:42:50 +1200 Subject: [PATCH] fix(vm): surface Proxmox error body instead of opaque status code The VM write path's SendFormAsync called Kiota's SendPrimitiveAsync with errorMapping: null, so any Proxmox 4xx/5xx threw a bare "no error factory is registered for this code: 500" and discarded Proxmox's actual message (e.g. "only root can set hostpciN config for non-mapped devices"). Register a catch-all 4XX/5XX error mapping that deserializes the error body into a new internal ProxmoxApiError (ApiException + IAdditionalDataHolder + IParsable, no declared fields so every returned field lands in AdditionalData), then rethrow an InvalidOperationException including the HTTP status code and every field Proxmox returned. Only the error path changes; the success path is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ProxmoxSharp/Vm/QemuWriter.cs | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/ProxmoxSharp/Vm/QemuWriter.cs b/src/ProxmoxSharp/Vm/QemuWriter.cs index 81608cb..300d105 100644 --- a/src/ProxmoxSharp/Vm/QemuWriter.cs +++ b/src/ProxmoxSharp/Vm/QemuWriter.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.Json; using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Abstractions.Serialization; using ProxmoxSharp.Api; namespace ProxmoxSharp.Vm; @@ -146,6 +147,14 @@ public async Task> ListPciAsync(string node, Cancellati // --- internals --------------------------------------------------------- + // Catch-all error mapping: parse ANY error status into ProxmoxApiError, whose + // IAdditionalDataHolder grabs every JSON field Proxmox returned (data/errors/message). + private static readonly Dictionary> ErrorMapping = new() + { + ["4XX"] = static _ => new ProxmoxApiError(), + ["5XX"] = static _ => new ProxmoxApiError(), + }; + private Dictionary PathParams(string node, int vmid) => new() { ["baseurl"] = BaseUrl, @@ -168,7 +177,21 @@ public async Task> ListPciAsync(string node, Cancellati ri.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(body)), "application/x-www-form-urlencoded"); } - var stream = await _adapter.SendPrimitiveAsync(ri, errorMapping: null, cancellationToken: ct).ConfigureAwait(false); + Stream? stream; + try + { + stream = await _adapter.SendPrimitiveAsync(ri, ErrorMapping, cancellationToken: ct).ConfigureAwait(false); + } + catch (ProxmoxApiError err) + { + // Surface Proxmox's actual error body — the generated client otherwise + // throws a bare "no error factory for " and discards the message. + var detail = err.AdditionalData.Count > 0 + ? string.Join("; ", err.AdditionalData.Select(kv => $"{kv.Key}={kv.Value}")) + : "(empty body)"; + throw new InvalidOperationException( + $"Proxmox {method} {urlTemplate} failed (HTTP {err.ResponseStatusCode}): {detail}", err); + } if (stream is null) return null; await using var _ = stream.ConfigureAwait(false); using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: ct).ConfigureAwait(false); @@ -180,3 +203,15 @@ public async Task> ListPciAsync(string node, Cancellati /// A PCI device reported by a node (subset relevant to passthrough). public sealed record PciDevice(string? Id, string? DeviceName, string? VendorName, long? IommuGroup); + +/// +/// Kiota error type for Proxmox failures. Implements +/// with no declared fields, so every property of the error body (data / errors / message) +/// lands in for surfacing. +/// +internal sealed class ProxmoxApiError : ApiException, IAdditionalDataHolder, IParsable +{ + public IDictionary AdditionalData { get; set; } = new Dictionary(); + public IDictionary> GetFieldDeserializers() => new Dictionary>(); + public void Serialize(ISerializationWriter writer) { } +}