-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* using ArrayPool<byte> instead of memorystream CopyToAsync * Maybe this could be a solution. It should be checked. Adding StreamHttpContent (yarp) * little bit of cleanup here * Avoiding ToLower() in IsSupportedHeader, using StringComparer.OrdinalIgnoreCase * for smaller payloads, avoid allocating a buffer that is larger than the announced content length * adding some unit tests for stream http content tests * typo * GivenThereIsAPossiblyBrokenServiceRunningOn * Some code refactorings after code review. There are still some todos, but providing some more improvements, removing the exception handling from RequestMapper. It's handled in the middleware now, we will need to analyze this in detail. * Some code cleanup * removing some commented code bits * adding more information in InvalidOperationException * changing some methods signatures in request mapper, making them static. * code review * Update gotchas.rst * Update notsupported.rst * Update gotchas.rst Add "Maximum request body size" notes * With this fix, the system is able to handle content with size > 2 Gb * adding new benchmarks, generating the payloads on the fly, from 1KB to 1024MB * Review PayloadBenchmarks * valid JSON * should make sure the payloads directory exists * Payloads folder name should match test method name --------- Co-authored-by: Raman Maksimchuk <dotnet044@gmail.com>
- Loading branch information
Showing
15 changed files
with
1,220 additions
and
770 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,9 @@ | ||
using Microsoft.AspNetCore.Http; | ||
using Ocelot.Configuration; | ||
using Ocelot.Responses; | ||
|
||
namespace Ocelot.Request.Mapper | ||
{ | ||
public interface IRequestMapper | ||
{ | ||
Task<Response<HttpRequestMessage>> Map(HttpRequest request, DownstreamRoute downstreamRoute); | ||
} | ||
|
||
namespace Ocelot.Request.Mapper; | ||
|
||
public interface IRequestMapper | ||
{ | ||
HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRoute); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
using Microsoft.AspNetCore.Http; | ||
using System.Buffers; | ||
|
||
namespace Ocelot.Request.Mapper; | ||
|
||
public class StreamHttpContent : HttpContent | ||
{ | ||
private const int DefaultBufferSize = 65536; | ||
public const long UnknownLength = -1; | ||
private readonly HttpContext _context; | ||
|
||
public StreamHttpContent(HttpContext context) | ||
{ | ||
_context = context ?? throw new ArgumentNullException(nameof(context)); | ||
} | ||
|
||
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context, | ||
CancellationToken cancellationToken) | ||
=> await CopyAsync(_context.Request.Body, stream, Headers.ContentLength ?? UnknownLength, false, | ||
cancellationToken); | ||
|
||
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) | ||
=> await CopyAsync(_context.Request.Body, stream, Headers.ContentLength ?? UnknownLength, false, | ||
CancellationToken.None); | ||
|
||
protected override bool TryComputeLength(out long length) | ||
{ | ||
length = -1; | ||
return false; | ||
} | ||
|
||
// This is used internally by HttpContent.ReadAsStreamAsync(...) | ||
protected override Task<Stream> CreateContentReadStreamAsync() | ||
{ | ||
// Nobody should be calling this... | ||
throw new NotImplementedException(); | ||
} | ||
|
||
private static async Task CopyAsync(Stream input, Stream output, long announcedContentLength, | ||
bool autoFlush, CancellationToken cancellation) | ||
{ | ||
// For smaller payloads, avoid allocating a buffer that is larger than the announced content length | ||
var minBufferSize = announcedContentLength != UnknownLength && announcedContentLength < DefaultBufferSize | ||
? (int)announcedContentLength | ||
: DefaultBufferSize; | ||
|
||
var buffer = ArrayPool<byte>.Shared.Rent(minBufferSize); | ||
long contentLength = 0; | ||
try | ||
{ | ||
while (true) | ||
{ | ||
// Issue a zero-byte read to the input stream to defer buffer allocation until data is available. | ||
// Note that if the underlying stream does not supporting blocking on zero byte reads, then this will | ||
// complete immediately and won't save any memory, but will still function correctly. | ||
var zeroByteReadTask = input.ReadAsync(Memory<byte>.Empty, cancellation); | ||
if (zeroByteReadTask.IsCompletedSuccessfully) | ||
{ | ||
// Consume the ValueTask's result in case it is backed by an IValueTaskSource | ||
_ = zeroByteReadTask.Result; | ||
} | ||
else | ||
{ | ||
// Take care not to return the same buffer to the pool twice in case zeroByteReadTask throws | ||
var bufferToReturn = buffer; | ||
buffer = null; | ||
ArrayPool<byte>.Shared.Return(bufferToReturn); | ||
|
||
await zeroByteReadTask; | ||
|
||
buffer = ArrayPool<byte>.Shared.Rent(minBufferSize); | ||
} | ||
|
||
var read = await input.ReadAsync(buffer.AsMemory(), cancellation); | ||
contentLength += read; | ||
|
||
// Normally this is enforced by the server, but it could get out of sync if something in the proxy modified the body. | ||
if (announcedContentLength != UnknownLength && contentLength > announcedContentLength) | ||
{ | ||
throw new InvalidOperationException($"More data ({contentLength} bytes) received than the specified Content-Length of {announcedContentLength} bytes."); | ||
} | ||
|
||
// End of the source stream. | ||
if (read == 0) | ||
{ | ||
if (announcedContentLength == UnknownLength || contentLength == announcedContentLength) | ||
{ | ||
return; | ||
} | ||
else | ||
{ | ||
throw new InvalidOperationException($"Sent {contentLength} request content bytes, but Content-Length promised {announcedContentLength}."); | ||
} | ||
} | ||
|
||
await output.WriteAsync(buffer.AsMemory(0, read), cancellation); | ||
if (autoFlush) | ||
{ | ||
// HttpClient doesn't always flush outgoing data unless the buffer is full or the caller asks. | ||
// This is a problem for streaming protocols like WebSockets and gRPC. | ||
await output.FlushAsync(cancellation); | ||
} | ||
} | ||
} | ||
finally | ||
{ | ||
if (buffer != null) | ||
{ | ||
ArrayPool<byte>.Shared.Return(buffer); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.