net接口请求参数可能会被拦截–巨坑

中间件引起的接口请求参数被拦截,导致参数一直是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 解决方案

读取并保留请求体内容供后续使用,你需要:

  1. 启用请求体缓冲:context.Request.EnableBuffering();
  2. 读取后重置流的位置: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 });
			});
		}
	}
}

如果本文介绍对你有帮助,可以一键四连:点赞+评论+收藏+推荐,谢谢!