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操作を行い、正常に読み取ることができます.
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()