C# REST PATCH接口JSON体传递Stream报错的解决方法
问题描述
我在C#中实现了一个REST MVC控制器的PATCH方法,请求体对应的DTO包含普通属性和Stream类型的文件属性:
public class MyDTO { public string SomeProperty { get; set; } public Stream SomeFile { get; set; } }
Swagger UI生成的示例JSON将SomeFile显示为字符串,客户端调用时,无论是赋值FileStream还是内存流,都会抛出错误:
Timeouts are not supported on this stream
对应的堆栈跟踪:
System.Net.Http.HttpRequestException : An error occurred while sending the request.
----> System.InvalidOperationException : Timeouts are not supported on this stream.
at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at ServiceClient.Classes.RestClient.SendPatchRequestNoAuth[TContentType](String url, TContentType content) in C:\Development\ServiceClient\Classes\RestClient.cs:line 58
解决方案
1. 改用Multipart/form-data传输(推荐)
JSON序列化器无法原生处理Stream类型,这是问题根源。正确的做法是用multipart/form-data格式混合传输普通属性和文件:
服务端调整
修改控制器方法,用[FromForm]接收表单数据,DTO改用IFormFile接收文件:
[HttpPatch] public async Task<IActionResult> PatchResource([FromForm] MyFormDto dto) { // 读取文件内容示例 using var reader = new StreamReader(dto.SomeFile.OpenReadStream()); var fileContent = await reader.ReadToEndAsync(); // 业务逻辑处理 return Ok(); } public class MyFormDto { public string SomeProperty { get; set; } public IFormFile SomeFile { get; set; } }
客户端调整
构建MultipartFormDataContent来组装请求内容:
public async Task SendPatchWithFile(string apiUrl, string propertyValue, Stream fileStream, string fileName) { using var formContent = new MultipartFormDataContent(); // 添加普通字符串属性 formContent.Add(new StringContent(propertyValue), "SomeProperty"); // 添加文件流,指定媒体类型 using var fileContent = new StreamContent(fileStream); fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream"); formContent.Add(fileContent, "SomeFile", fileName); using var httpClient = new HttpClient(); var response = await httpClient.PatchAsync(apiUrl, formContent); response.EnsureSuccessStatusCode(); }
2. 转Base64字符串传输(仅适合小文件)
如果必须用JSON格式,可以将Stream转换为Base64字符串:
修改DTO
public class MyDto { public string SomeProperty { get; set; } public string SomeFileBase64 { get; set; } // 替换Stream为Base64字符串 }
客户端处理
将文件流转为Base64后序列化JSON:
// 把文件流复制到内存流再转Base64 using var memoryStream = new MemoryStream(); await fileStream.CopyToAsync(memoryStream); var fileBase64 = Convert.ToBase64String(memoryStream.ToArray()); var dto = new MyDto { SomeProperty = "测试值", SomeFileBase64 = fileBase64 }; // 发送JSON格式的PATCH请求 using var httpClient = new HttpClient(); var jsonContent = JsonContent.Create(dto); var response = await httpClient.PatchAsync(apiUrl, jsonContent); response.EnsureSuccessStatusCode();
服务端还原
[HttpPatch] public IActionResult PatchResource([FromBody] MyDto dto) { // 将Base64转回Stream var fileBytes = Convert.FromBase64String(dto.SomeFileBase64); var fileStream = new MemoryStream(fileBytes); // 业务逻辑处理 return Ok(); }
内容的提问来源于stack exchange,提问作者Britonk

