实际应用场景是将用户上传的文件依次保存到阿里云 OSS 与腾讯云 COS ,实现方式是在启用 Request.EnableRewind() 的情况下通过 Request.Body 读取流,并依次通过 2 个 StreamContent 分别上传到阿里云 OSS 与 腾讯云 COS ,在集成测试中可以正常上传(用的是 TestServer 启动站点),而部署到服务器上通过浏览器上传却出现了奇怪的问题 —— 第一个 StreamContent 上传后的文件大小总是0,而第二个 StreamContent 上传正常。上传文件大小为 0 时,对应的 Request.Body.Length 也为 0 。(注:如果不使用 Request.EnableRewind ,Request.Body 只能被读取一次)

而如果在第一个 StreamContent 读取 Request.Body 之前先通过 MemoryStream 进行一次流的 Copy 操作,就能正常读取。

using (var ms = new MemoryStream())
{
await Request.Body.CopyToAsync(ms);
}

好奇怪的问题!要牺牲第一个流,才能让后面的 StreamContent 从 Request.Body 中读到数据。 为什么会这样?

先从 Request.EnableRewind() 下手,通过它的实现源码知道了 EnableRewind 之后 Request.Body 被替换为 FileBufferingReadStream ,所以 StreamContent 实际读取的是 FileBufferingReadStream ,问题可能与 FileBufferingReadStream 有关。

public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
{
//..
var body = request.Body;
if (!body.CanSeek)
{
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
request.Body = fileStream;
request.HttpContext.Response.RegisterForDispose(fileStream);
}
return request;
}

向前进,查看 FileBufferingReadStream 的实现源码。

在构造函数中 _buffer 的长度被设置为 0 :

if (memoryThreshold < _maxRentedBufferSize)
{
_rentedBuffer = bytePool.Rent(memoryThreshold);
_buffer = new MemoryStream(_rentedBuffer);
_buffer.SetLength();
}
else
{
_buffer = new MemoryStream();
}

FileBufferingReadStream 的长度实际就是 _buffer 的长度:

public override long Length
{
get { return _buffer.Length; }
}

ReadAsync 读取流的代码(已移除不相关的代码):

public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ThrowIfDisposed();
if (_buffer.Position < _buffer.Length || _completelyBuffered)
{
// Just read from the buffer
return await _buffer.ReadAsync(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position), cancellationToken);
} int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken);
//...
if (read > )
{
await _buffer.WriteAsync(buffer, offset, read, cancellationToken);
}
//...
return read;
}

从 FileBufferingReadStream 的实现代码中没有发现问题。

只有 StreamContent 才会出现这个问题吗?写了个简单的 ASP.NET Core 程序验证了一下:

public async Task<IActionResult> Index()
{
Request.EnableRewind(); Request.Body.Seek(, );
Console.WriteLine("First Read Request.Body");
await Request.Body.CopyToAsync(Console.OpenStandardOutput());
Console.WriteLine(); Request.Body.Seek(, );
Console.WriteLine("Second Read Request.Body");
await Request.Body.CopyToAsync(Console.OpenStandardOutput());
Console.WriteLine(); Request.Body.Seek(, );
using (var sr = new StreamReader(Request.Body))
{
return Ok(await sr.ReadToEndAsync());
}
}

控制台输出流(System.ConsolePal+WindowsConsoleStream)没这个问题。

是 StreamContent 的问题吗?用下面的代码验证一下

public async Task<IActionResult> Index()
{
Request.EnableRewind();
var streamContent = new StreamContent(Request.Body);
return Ok(await streamContent.ReadAsStringAsync());
}

奇怪了,StreamContnent 也没问题,只是剩下唯一的嫌疑对象 —— HttpClient 。

写测试代码进行验证,站点A的代码(监听于5000端口)

public async Task<IActionResult> Index()
{
Request.EnableRewind();
var streamContent = new StreamContent(Request.Body);
var httpClient = new HttpClient();
var response = await httpClient.PostAsync("http://localhost:5002", streamContent);
return Ok(await response.Content.ReadAsStringAsync());
}

站点B的代码(监听于5002端口)

public async Task<IActionResult> Index()
{
using (var ms = new MemoryStream())
{
await Request.Body.CopyToAsync(ms);
return Ok(ms.Length);
}
}

站点 A 启用 EnableRewind 并直接将 Request.Body 流 POST 到站点 B ,模拟实际应用场景。

测试得到的返回值是 0 ,问题重现了。

为了进一步验证是否是 HttpClient 的问题,将 HttpClient 改为 WebRequest 。

public async Task<IActionResult> Index()
{
Request.EnableRewind();
var request = WebRequest.CreateHttp("http://localhost:5002");
request.Method = "POST";
using (var requestStream = await request.GetRequestStreamAsync())
{
await Request.Body.CopyToAsync(requestStream);
}
using (var response = await request.GetResponseAsync())
{
using (var sr = new StreamReader(response.GetResponseStream()))
{
return Ok(await sr.ReadToEndAsync());
}
}
}

测试结果显示 WebRequest 没这个问题,果然与 HttpClient 有关。

向 HttpClient 的源代码进军。。。

从 HttpClient.SendAsync 到 HttpMessageInvoker.SendAsync 再到 HttpMessageHandler.SendAsync ,默认用的是 SocketsHttpHandler ,从 SocketsHttpHandler.SendAsync 到 HttpConnectionHandler.SendAsync 到 HttpConnectionPoolManager.SendAsync 。。。翻山越岭,长途跋涉,来到了 HttpConnection 的 SendAsyncCore 方法。

public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
{
//..
await SendRequestContentAsync(request, CreateRequestContentStream(request), cancellationToken).ConfigureAwait(false);
//...
}

原来是调用 SendRequestContentAsync 方法发送请求内容的

private async Task SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, CancellationToken cancellationToken)
{
// Now that we're sending content, prohibit retries on this connection.
_canRetry = false; // Copy all of the data to the server.
await request.Content.CopyToAsync(stream, _transportContext, cancellationToken).ConfigureAwait(false); // Finish the content; with a chunked upload, this includes writing the terminating chunk.
await stream.FinishAsync().ConfigureAwait(false); // Flush any content that might still be buffered.
await FlushAsync().ConfigureAwait(false);
}

从 request.Content.CopyToAsync 追踪到 HttpConnection 的  WriteAsync 方法

private async Task WriteAsync(ReadOnlyMemory<byte> source)
{
int remaining = _writeBuffer.Length - _writeOffset; if (source.Length <= remaining)
{
// Fits in current write buffer. Just copy and return.
WriteToBuffer(source);
return;
} if (_writeOffset != )
{
// Fit what we can in the current write buffer and flush it.
WriteToBuffer(source.Slice(, remaining));
source = source.Slice(remaining);
await FlushAsync().ConfigureAwait(false);
} if (source.Length >= _writeBuffer.Length)
{
// Large write. No sense buffering this. Write directly to stream.
await WriteToStreamAsync(source).ConfigureAwait(false);
}
else
{
// Copy remainder into buffer
WriteToBuffer(source);
}
}

看这部分代码实在毫无头绪,于是采用笨方法——手工打点在控制台显示信息,在 WriteToStreamAsync 进行打点

private ValueTask WriteToStreamAsync(ReadOnlyMemory<byte> source)
{
if (NetEventSource.IsEnabled) Trace($"Writing {source.Length} bytes.");
Console.WriteLine($"{_stream} Writing {source.Length} bytes.");
Console.WriteLine("source text: " + System.Text.Encoding.Default.GetString(source.ToArray()));
return _stream.WriteAsync(source);
}

编译 System.Net.Http 解决方案,将编译输出的 corefx\bin\Windows_NT.AnyCPU.Debug\System.Net.Http\netcoreapp\System.Net.Http.dll 复制到 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.2 文件夹中,然后就可以使用自己编译的 System.Net.Http.dll 运行 ASP.NET Core 程序。

运行测试站点(见之前的站点A与站点B的代码,站点 A 将 Request.Body 流中的内容通过 HttpClient POST 到站点 B ),站点 A 的控制台显示了下面的打点信息:

System.Net.Sockets.NetworkStream Writing 10 bytes.
source text: POST / HTT
System.Net.Sockets.NetworkStream Writing 10 bytes.
source text: P/1.1
Con
System.Net.Sockets.NetworkStream Writing 10 bytes.
source text: tent-Lengt
System.Net.Sockets.NetworkStream Writing 10 bytes.
source text: h: 0
Host
System.Net.Sockets.NetworkStream Writing 10 bytes.
source text: : localhos
_writeBuffer.Length: 10
_writeOffset: 10
remaining: 0
source.Length: 4
System.Net.Sockets.NetworkStream Writing 10 bytes.
source text: t:5002 System.Net.Sockets.NetworkStream Writing 4 bytes.
source text: test

将上面的 source text 内容连接起来,到下面的 http 请求内容:

POST / HTTP/1.1
Content-Length: 0
Host: localhost:5002 test

立马就发现了问题:Content-Length: 0 ,原来是 Content-Length 惹的祸,怎么会是 0 ?

继续打点。。。

找到了 "Content-Length: 0" 是  StreamContent 中的 TryComputeLength 方法引起的

protected internal override bool TryComputeLength(out long length)
{
if (_content.CanSeek)
{
length = _content.Length - _start;
return true;
}
else
{
length = ;
return false;
}
}

上面的代码中 _content.Length 的值为 0 (在博文的开头我们提到过 FileBufferingReadStream 在未被读取时 Length 的值为 0 ),于是 length 为 0 并返回 true ,所以生成了 "Content-Length: 0" 请求头。

如果当 length 为 0 时,让 TryComputeLength 返回 false ,这样就不会生成 "Content-Length: 0" 请求头,是不是可以解决问题呢?

protected internal override bool TryComputeLength(out long length)
{
if (_content.CanSeek)
{
length = _content.Length - _start;
return length > ;
}
else
{
length = ;
return false;
}
}

这样会产生下面的请求内容:

POST / HTTP/1.1
Transfer-Encoding: chunked
Host: localhost:5002 4
test
0

这样的请求内容在示例程序服务端就可以正常读取到 Request.Body ,但是无法将文件上传到阿里云 OSS 与腾讯云 COS ,应该是 "Transfer-Encoding: chunked" 请求头的原因。

后来改为从 HttpAbstractions 下手,修改了 BufferingHelper.cs 与 FileBufferingReadStream.cs 的代码,终于解决了这个问题。

给 FileBufferingReadStream.cs 添加一个私有字段 _innerLength ,在 Request.EnableRewind 时通过构造函数将 Request.ContentLength 的值传给 _innerLength 。

var fileStream = new FileBufferingReadStream(body, request.ContentLength, bufferThreshold, bufferLimit, _getTempDirectory);

在 FileBufferingReadStream 的 Length 属性中,如果流还没被读取过,就返回 _innerLength 的值。

public override long Length
{
get
{
var useInnerLength = _innerLength.HasValue && _innerLength >
&& !_completelyBuffered && _buffer.Position == ;
return useInnerLength ?_innerLength.Value : _buffer.Length;
}
}

修改 HttpAbstractions 的源代码后,需要将编译生成的下面5个文件都复制到 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.2 文件夹中。

Microsoft.AspNetCore.Http.Abstractions.dll
Microsoft.AspNetCore.Http.dll
Microsoft.AspNetCore.Http.Features.dll
Microsoft.Net.Http.Headers.dll
Microsoft.AspNetCore.WebUtilities.dll

如果用的是 Linux ,需要复制到 /usr/share/dotnet/shared/Microsoft.AspNetCore.App/2.1.2/ 目录中。

后来发现基于 request.ContentLength 的解决方法不适用于 chunked requests ,将 CanSeek 属性改为下面的实现(原先是直接返回true)

public override bool CanSeek
{
get { return Length > ; }
}

这样第一读 Request.Body 正常,但之后继续读会出现下面的错误:

System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'FileBufferingReadStream'.
at WebsiteA.FileBufferingReadStream.ThrowIfDisposed()

ASP.NET Core 问题排查:Request.EnableRewind 后第一次读取不到 Request.Body的更多相关文章

  1. ASP.NET Core 入门(2)(WebApi接口请求日志 Request和Response)

    以前 .NET Framework WebApi 记录接口访问日志,一般是通过Filter的方式进行拦截,通过重写ActionFilterAttribute的OnActionExecuting实现拦截 ...

  2. [ASP.NET Core 3框架揭秘] 配置[2]:读取配置数据[下篇]

    [接上篇]提到“配置”二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化的配置定义 ...

  3. ASP.NET Core的配置(1):读取配置信息

    提到"配置"二字,我想绝大部分.NET开发人员脑海中会立马浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化 ...

  4. [ASP.NET Core 3框架揭秘] 配置[1]:读取配置数据[上篇]

    提到"配置"二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化 ...

  5. asp.net core轻松入门之MVC中Options读取配置文件

    接上一篇中讲到利用Bind方法读取配置文件 ASP.NET Core轻松入门Bind读取配置文件到C#实例 那么在这篇文章中,我将在上一篇文章的基础上,利用Options方法读取配置文件 首先注册MV ...

  6. Asp.net Core中使用Redis 来保存Session, 读取配置文件

    今天 无意看到Asp.net Core中使用Session ,首先要使用Session就必须添加Microsoft.AspNetCore.Session包,默认Session是只能存去字节,所以如果你 ...

  7. 温故知新,使用ASP.NET Core创建Web API,永远第一次

    ASP.NET Core简介 ASP.NET Core是一个跨平台的高性能开源框架,用于生成启用云且连接Internet的新式应用. 使用ASP.NET Core,您可以: 生成Web应用和服务.物联 ...

  8. 使用Enablebuffering多次读取Asp Net Core 3.0 请求体 读取Request.Body流

    原文:使用Enablebuffering多次读取Asp Net Core 请求体 使用Enablebuffering多次读取Asp Net Core 请求体 1 .Net Core 2.X时代 使用E ...

  9. ASP.NET Core 如何设置发布环境

    在ASP.NET Core中自带了一些内置对象,可以读取到当前程序处于什么样的环境当中,比如在ASP.NET Core的Startup类的Configure方法中,我们就会看到这么一段代码: publ ...

随机推荐

  1. 浅析UPnP协议

    摘要:文章介绍了UPnP结构规范和开发流程,指出:UP nP协议使所有联网的设备实现互联,设备工作不需要传统的驱动程序,便可以实现设备间的 相互控制. 关键词:UPnP协议:开发流程:电子技术:工作 ...

  2. 我所了解的各公司使用的 Ceph 存储集群 (携程、乐视、有云、宝德云、联通等)

    Ceph 作为软件定义存储的代表之一,最近几年其发展势头很猛,也出现了不少公司在测试和生产系统中使用 Ceph 的案例,尽管与此同时许多人对它的抱怨也一直存在.本文试着整理作者了解到的一些使用案例. ...

  3. 摘抄——读《大话移动APP测试 Android与IOS》

    用了两天读完了<大话移动APP测试 Android与IOS>,由于刚开始接触移动测试,技术型的篇章只能先放过了o(╯□╰)o,有以下内容觉得很有必要时不时的看看,来反思自己的工作,自勉!! ...

  4. iOS-OC-基础-NSPredicate常用方法

    NSpredicate 常用方法 // 谓词的条件查询 > .< .==.!= NSPredicate *predicate1 = [NSPredicate predicateWithFo ...

  5. 运行tomcat6w.exe ,提示 指定的服务未安装 unable to open the service 'tomcat66'

    错误:运行tomcat6w.exe ,提示 指定的服务未安装 unable to open the service 'tomcat6'(我用的是官网下载的解压版) 解决方法: 打开命令行提示符窗口=& ...

  6. StringBuffer的delete方法与deleteCharAt方法的区别。

    delete方法与deleteCharAt两个方法都是用来删除StringBuffer字符串指定索引字符的方法, delete(int  begin,int  end)有两个参数,使用时删除索引从be ...

  7. 11、Redis的持久化(RDB、AOF)

    写在前面的话:读书破万卷,编码如有神 --------------------------------------------------------------------------------- ...

  8. 分布式理论系列(二)一致性算法:2PC 到 3PC 到 Paxos 到 Raft 到 Zab

    分布式理论系列(二)一致性算法:2PC 到 3PC 到 Paxos 到 Raft 到 Zab 本文介绍一致性算法: 2PC 到 3PC 到 Paxos 到 Raft 到 Zab 两类一致性算法(操作原 ...

  9. 2018-2019-2 20165219《网络对抗技术》Exp2 后门原理与实践

    2018-2019-2 20165219<网络对抗技术>Exp2 后门原理与实践 实验内容 使用netcat获取主机操作Shell,cron启动 使用Socat获取主机操作Shell, 任 ...

  10. Android 切换主题 (二)

    Android 切换主题 (二) 背景 我原来写过一篇文章关于 android 切换主题的文章 -- Android 切换主题以及换肤的实现 , 里面介绍了如何使用 setTheme() 来切换主题, ...