ASP.NET Core問題の調査:Request.EnableRewind以降初めてRequestが読み取れない.Body
23406 ワード
実際の応用シーンは、ユーザーがアップロードしたファイルをアリクラウドOSSとテンセントクラウドCOSに順次保存し、Requestを有効にする.EnableRewind()の場合Request.Bodyはストリームを読み取り、2つのStreamContentを介してアリクラウドOSSとテンセントクラウドCOSに順次アップロードし、統合テストで正常にアップロードできる(TestServer起動サイトを使用している)が、サーバに配備されてブラウザでアップロードすると奇妙な問題が発生した.最初のStreamContentアップロード後のファイルサイズは常に0であり、2番目のStreamContentアップロードは正常である.アップロードファイルのサイズが0の場合、対応するRequest.Body.Lengthも0です.(注意:Request.EnableRewindを使用しない場合、Request.Bodyは一度しか読み込めません)
そして、最初のStreamContentでRequestを読み出す.BodyはまずMemoryStreamで1回のストリームのCopy操作を行い、正常に読み取ることができます.
変な質問ですね.最初のストリームを犠牲にしてこそ、後ろのStreamContentがRequestから離れることができる.Bodyでデータを読みました.どうしてこんなことになったの?
まずRequestからEnableRewind()が着手し、その実現ソースコードを通じてEnableRewindの後Requestを知る.BodyはFileBufferingReadStreamに置き換えられたため、StreamContentが実際に読み込んだのはFileBufferingReadStreamであり、問題はFileBufferingReadStreamに関連する可能性がある.
進むには、FileBufferingReadStreamの実装ソースコードを表示します.
コンストラクション関数で_bufferの長さは0に設定されています.
FileBufferingReadStreamの長さは実際には_bufferの長さ:
ReadAsyncはストリームのコードを読み込みます(関連しないコードは削除されました):
FileBufferingReadStreamの実装コードから問題は見つかりませんでした.
この問題が発生するのはStreamContentだけですか?簡単なASPを書きました.NET Coreプログラムで検証しました.
コンソール出力ストリーム(System.consolePal+WindowsConsoleStream)に問題はありません.
StreamContentの問題ですか?次のコードで検証してください.
不思議なことに、StreamContnentも大丈夫で、唯一の容疑者であるHttpClientが残っているだけだ.
テストコードを書いて検証し、サイトAのコード(5000ポートで傍受)
サイトBのコード(5002ポートで傍受)
サイトAはEnableRewindを有効にし、Requestを直接使用します.BodyストリームPOSTはサイトBに流れ、実際の応用シーンをシミュレートする.
テストで得られた戻り値は0で、問題が再現されました.
HttpClientの問題であるかどうかをさらに検証するために、HttpClientをWebRequestに変更します.
テストの結果、WebRequestはこの問題がなく、やはりHttpClientと関係があることが明らかになった.
HttpClientのソースコードに進出..
HttpClient.からSendAsyncからHttpMessageInvokerへSendAsyncはまたHttpMessageHandlerに着きます.SendAsync、デフォルトはSocketsHttpHandler、SocketsHttpHandlerです.SendAsyncからHttpConnectionHandlerへSendAsyncからHttpConnectionPoolManagement.へSendAsync ...峠を越えて、長旅して、HttpConnectionのSendAsyncCoreメソッドにやってきました.
SendRequestContentAsyncメソッドを呼び出してリクエスト内容を送信したのです
requestからContent.CopyToAsyncがHttpConnectionに追跡するWriteAsyncメソッド
このコードを見るとまったく見当がつかないので、手動でコンソールに情報を表示し、WriteToStreamAsyncで打点する愚かな方法を採用しました.
コンパイルシステムNet.Httpソリューション、出力corefxbinWindows_をコンパイルNT.AnyCPU.Debug\System.Net.Httpetcoreapp\System.Net.Http.dllをC:Program FilesdotnetsharedMicrosoftにコピーします.NETCore.App2.1.2フォルダでは、自分でコンパイルしたSystemを使用できます.Net.Http.dll運転ASP.NET Coreプログラム.
テストサイトを実行します(前のサイトAとサイトBのコードを参照してください.サイトAはRequest.Bodyストリームの内容をHttpClient POSTをサイトBに通過します).サイトAのコンソールには次の打点情報が表示されます.
上のsource textコンテンツを接続し、下のhttpにコンテンツを要求します.
すぐに問題を発見しました:Content-Longth:0、もとはContent-Longthが引き起こした災いで、どうして0ですか?
打点を続けます...
「Content-Longth:0」は、StreamContentのTryComputeLengthメソッドによって引き起こされたものです.
上のコードの中で_content.Lengthの値は0(ブログの冒頭でFileBufferingReadStreamが読み込まれていないときのLengthの値が0であることに言及した)であり、lengthが0でtrueに戻るため、Content-Length:0要求ヘッダが生成される.
lengthが0の場合、TryComputeLengthにfalseを返させると、Content-Longth:0リクエストヘッダが生成されず、問題を解決できるのではないでしょうか.
これにより、次のリクエストが生成されます.
このような要求内容は、サンプルプログラムのサービス側でRequestに正常に読み取ることができる.Bodyですが、アリクラウドOSSとテンセントクラウドCOSにファイルをアップロードできないのは、「Transfer-Encoding:chunked」リクエストヘッダの原因であるはずです.
その後、HttpAbstractionsから手をつけ、BufferingHelperを修正した.csとFileBufferingReadStream.csのコードは、ついにこの問題を解決した.
FileBufferingReadStreamへcsプライベートフィールドを追加_RequestでEnableRewindでは構造関数でRequest.ContentLengthの値を_に渡すinnerLength .
FileBufferingReadStreamのLengthプロパティで、ストリームがまだ読み込まれていない場合は_を返します.innerLengthの値.
HttpAbstractionsのソースコードを変更すると、コンパイルして生成した次の5つのファイルをC:Program FilesdotnetsharedMicrosoftにコピーする必要があります.NETCore.App2.1.2フォルダにあります.
Linuxを使用する場合は、/usr/share/dotnet/shared/Microsoftにコピーする必要があります.AspNetCore.App/2.1.2/ディレクトリにあります.
その後requestに基づいて発見された.ContentLengthの解決策はchunked requestsには適用されず、CanSeek属性を次の実装に変更します(元は直接trueに戻ります)
このように最初に読むBodyは正常ですが、その後読み続けると次のエラーが発生します.
そして、最初のStreamContentでRequestを読み出す.BodyはまずMemoryStreamで1回のストリームの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(0);
}
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 > 0)
{
await _buffer.WriteAsync(buffer, offset, read, cancellationToken);
}
//...
return read;
}
FileBufferingReadStreamの実装コードから問題は見つかりませんでした.
この問題が発生するのはStreamContentだけですか?簡単なASPを書きました.NET Coreプログラムで検証しました.
public async Task Index()
{
Request.EnableRewind();
Request.Body.Seek(0, 0);
Console.WriteLine("First Read Request.Body");
await Request.Body.CopyToAsync(Console.OpenStandardOutput());
Console.WriteLine();
Request.Body.Seek(0, 0);
Console.WriteLine("Second Read Request.Body");
await Request.Body.CopyToAsync(Console.OpenStandardOutput());
Console.WriteLine();
Request.Body.Seek(0, 0);
using (var sr = new StreamReader(Request.Body))
{
return Ok(await sr.ReadToEndAsync());
}
}
コンソール出力ストリーム(System.consolePal+WindowsConsoleStream)に問題はありません.
StreamContentの問題ですか?次のコードで検証してください.
public async Task Index()
{
Request.EnableRewind();
var streamContent = new StreamContent(Request.Body);
return Ok(await streamContent.ReadAsStringAsync());
}
不思議なことに、StreamContnentも大丈夫で、唯一の容疑者であるHttpClientが残っているだけだ.
テストコードを書いて検証し、サイトAのコード(5000ポートで傍受)
public async Task 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 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 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からHttpConnectionPoolManagement.へSendAsync ...峠を越えて、長旅して、HttpConnectionのSendAsyncCoreメソッドにやってきました.
public async Task 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 != 0)
{
// Fit what we can in the current write buffer and flush it.
WriteToBuffer(source.Slice(0, 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);
}
コンパイルシステムNet.Httpソリューション、出力corefxbinWindows_をコンパイルNT.AnyCPU.Debug\System.Net.Httpetcoreapp\System.Net.Http.dllをC:Program FilesdotnetsharedMicrosoftにコピーします.NETCore.App2.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-Longth:0、もとはContent-Longthが引き起こした災いで、どうして0ですか?
打点を続けます...
「Content-Longth:0」は、StreamContentのTryComputeLengthメソッドによって引き起こされたものです.
protected internal override bool TryComputeLength(out long length)
{
if (_content.CanSeek)
{
length = _content.Length - _start;
return true;
}
else
{
length = 0;
return false;
}
}
上のコードの中で_content.Lengthの値は0(ブログの冒頭でFileBufferingReadStreamが読み込まれていないときのLengthの値が0であることに言及した)であり、lengthが0でtrueに戻るため、Content-Length:0要求ヘッダが生成される.
lengthが0の場合、TryComputeLengthにfalseを返させると、Content-Longth:0リクエストヘッダが生成されず、問題を解決できるのではないでしょうか.
protected internal override bool TryComputeLength(out long length)
{
if (_content.CanSeek)
{
length = _content.Length - _start;
return length > 0;
}
else
{
length = 0;
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プライベートフィールドを追加_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 > 0
&& !_completelyBuffered && _buffer.Position == 0;
return useInnerLength ?_innerLength.Value : _buffer.Length;
}
}
HttpAbstractionsのソースコードを変更すると、コンパイルして生成した次の5つのファイルをC:Program FilesdotnetsharedMicrosoftにコピーする必要があります.NETCore.App2.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 > 0; }
}
このように最初に読むBodyは正常ですが、その後読み続けると次のエラーが発生します.
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'FileBufferingReadStream'.
at WebsiteA.FileBufferingReadStream.ThrowIfDisposed()