中间件引起的接口请求参数被拦截,导致参数一直是null,这问题困扰了我很久,值得记录
1.场景
1.1 客户端使用framework4.8做一个接口请求发送:
public static class ApiHelper
{
private static string Internal_ApiUrl = string.Empty;
private static string Client_ApiUrl = string.Empty;
static ApiHelper()
{
Internal_ApiUrl = ConfigurationManager.AppSettings["Internal_ApiUrl"];
Client_ApiUrl = ConfigurationManager.AppSettings["Client_ApiUrl"];
}
public static string GetLicenseUrl()
{
return Internal_ApiUrl + "/Api/License/GetLicense";
}
public static WebApiCallBack GetLicense(string enterpriseName, string uniqueCode,bool IsExistLicense)
{
FMLicense fMLicense = new FMLicense { enterpriseName = enterpriseName, uniqueCode = uniqueCode, isExistLicense = IsExistLicense };
var jsonBody = JsonConvert.SerializeObject(fMLicense, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
return RequestSend(GetLicenseUrl(), "POST", jsonBody);
}
public static WebApiCallBack RequestSend(string serviceUrl, string method, string bodyJson)
{
ServicePointManager.Expect100Continue = false;
var handler = new HttpClientHandler();
using (var client = new HttpClient(handler))
{
Console.WriteLine(bodyJson);
var content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = client.PostAsync(serviceUrl, content).Result;
string result = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(result);
return JsonConvert.DeserializeObject<WebApiCallBack>(result);
}
}
}
1.2 服务端
[ApiController]
[Route("Api/[controller]/[action]")]
public class LicenseController : ControllerBase
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILicenseService _licenseService;
public LicenseController(ILicenseService licenseService, IHttpContextAccessor httpContextAccessor)
{
this._httpContextAccessor = httpContextAccessor;
this._licenseService = licenseService;
}
[HttpPost]
public async Task<WebApiCallBack> GetLicense(FMLicense License)
{
FMLicense fMLicense = License;
var result = new WebApiCallBack();
if (fMLicense == null)
{
result.code = GlobalStatusCodes.Status400BadRequest;
result.msg = "实体参数为空";
return result;
}
else
{
#region # 验证
if (string.IsNullOrEmpty(fMLicense.enterpriseName))
{
result.code = GlobalStatusCodes.Status400BadRequest;
result.msg = "实体参数为空";
result.otherData = fMLicense;
return result;
}
if (string.IsNullOrEmpty(fMLicense.uniqueCode))
{
result.code = GlobalStatusCodes.Status400BadRequest;
result.msg = "机器唯一码不可为空!";
result.otherData = fMLicense;
return result;
}
#endregion
//业务逻辑
return result;
}
}
1.3 写了一个中间件RequRespLogMildd ,记录请求和返回数据的日志
public class RequRespLogMildd
{
private readonly RequestDelegate _next;
public RequRespLogMildd(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (AppSettingsConstVars.MiddlewareRequestResponseLogEnabled)
{
// 过滤,只有接口
if (context.Request.Path.Value.Contains("api") || context.Request.Path.Value.Contains("Api"))
{
//context.Request.EnableBuffering();
Stream originalBody = context.Response.Body;
try
{
// 存储请求数据
await RequestDataLog(context);
using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await _next(context);
// 存储响应数据
ResponseDataLog(context.Response, ms);
ms.Position = 0;
await ms.CopyToAsync(originalBody);
}
}
catch (Exception ex)
{
// 记录异常
//ErrorLogData(context.Response, ex);
Parallel.For(0, 1, e =>
{
LogLockHelper.OutErrorLog("ErrorLog", "ErrorLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new string[] { "Request Data:", ex.Message, ex.StackTrace });
});
}
finally
{
context.Response.Body = originalBody;
}
}
else
{
await _next(context);
}
}
else
{
await _next(context);
}
}
private async Task RequestDataLog(HttpContext context)
{
var request = context.Request;
var sr = new StreamReader(request.Body);
var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{await sr.ReadToEndAsync()}";
if (!string.IsNullOrEmpty(content))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new string[] { "Request Data:", content });
});
//request.Body.Position = 0;
}
}
private void ResponseDataLog(HttpResponse response, MemoryStream ms)
{
ms.Position = 0;
var ResponseBody = new StreamReader(ms).ReadToEnd();
// 去除 Html
var reg = "<[^>]+>";
var isHtml = Regex.IsMatch(ResponseBody, reg);
if (!string.IsNullOrEmpty(ResponseBody))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new string[] { "Response Data:", ResponseBody });
});
}
}
}
以上中间件,在后端Program类中使用 app.UseRequestResponseLog();
不管使用客户端/postman/apifox 调用接口GetLicense时都会报错,请求的json格式一直错误,错误信息如下
{
"errors": {
"": [
"A non-empty request body is required."
],
"license": [
"The License field is required."
]
},
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-deaf4252040738321b26fc1fd3718696-ea7ecf0fa2bb0491-00"
}
1.4 错误原因是 Web API pipeline 被修改
某些中间件可能拦截请求流(body),例如使用了某些日志中间件或反复读取 body 的 filter,可能会导致模型绑定失败。检查 Startup.cs 或 Program.cs 中是否有读取 Request.Body 的地方。
ASP.NET Core 的模型绑定器只能读取一次 HttpRequest.Body。你在 RequestDataLog() 中读取了 Body,但没有重置流的位置:
var sr = new StreamReader(request.Body);
var content = $"... {await sr.ReadToEndAsync()}";
之后没有重置 request.Body.Position = 0;,所以模型绑定器读到的是空流。
2 解决方案
要 读取并保留请求体内容供后续使用,你需要:
- 启用请求体缓冲:context.Request.EnableBuffering();
- 读取后重置流的位置:request.Body.Position = 0;
优化后的代码:
public class RequRespLogMildd
{
private readonly RequestDelegate _next;
public RequRespLogMildd(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (AppSettingsConstVars.MiddlewareRequestResponseLogEnabled)
{
if (context.Request.Path.Value.Contains("api", StringComparison.OrdinalIgnoreCase))
{
Stream originalBody = context.Response.Body;
try
{
// 启用请求体缓冲
context.Request.EnableBuffering();
// 存储请求数据
await RequestDataLog(context);
using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await _next(context);
// 存储响应数据
ResponseDataLog(context.Response, ms);
ms.Position = 0;
await ms.CopyToAsync(originalBody);
}
}
catch (Exception ex)
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutErrorLog("ErrorLog", "ErrorLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new[] { "Request Data:", ex.Message, ex.StackTrace });
});
}
finally
{
context.Response.Body = originalBody;
}
}
else
{
await _next(context);
}
}
else
{
await _next(context);
}
}
private async Task RequestDataLog(HttpContext context)
{
var request = context.Request;
// 读取前先设置 Position = 0
request.Body.Position = 0;
//// leaveOpen: true 确保读取后流还可以被使用
//是否根据字节顺序标记(BOM)来检测编码:true(默认)检测 BOM,如果发现 BOM,则用它指定的编码代替传入的 Encoding.UTF8;false 不检测 BOM,严格使用你传入的 Encoding.UTF8。
using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var body = await reader.ReadToEndAsync();
// 读取完之后重置位置供后续使用
request.Body.Position = 0;
var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{body}";
if (!string.IsNullOrEmpty(content))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new[] { "Request Data:", content });
});
}
}
private void ResponseDataLog(HttpResponse response, MemoryStream ms)
{
ms.Position = 0;
var ResponseBody = new StreamReader(ms).ReadToEnd();
var reg = "<[^>]+>";
var isHtml = Regex.IsMatch(ResponseBody, reg);
if (!string.IsNullOrEmpty(ResponseBody))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new[] { "Response Data:", ResponseBody });
});
}
}
}
如果本文介绍对你有帮助,可以一键四连:点赞+评论+收藏+推荐,谢谢!