Skip to content

Commit 4dae9d4

Browse files
authored
Allow HttpSys zero-byte reads #41305 (#41500)
1 parent 1d83477 commit 4dae9d4

File tree

3 files changed

+68
-24
lines changed

3 files changed

+68
-24
lines changed

src/Servers/HttpSys/src/RequestProcessing/RequestStream.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ private void ValidateReadBuffer(byte[] buffer, int offset, int size)
102102
{
103103
throw new ArgumentNullException(nameof(buffer));
104104
}
105-
if (offset < 0 || offset > buffer.Length)
105+
if ((uint)offset > (uint)buffer.Length)
106106
{
107107
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
108108
}
109-
if (size <= 0 || size > buffer.Length - offset)
109+
if ((uint)size > (uint)(buffer.Length - offset))
110110
{
111111
throw new ArgumentOutOfRangeException(nameof(size), size, string.Empty);
112112
}
@@ -163,7 +163,14 @@ public override unsafe int Read([In, Out] byte[] buffer, int offset, int size)
163163

164164
dataRead += extraDataRead;
165165
}
166-
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
166+
167+
// Zero-byte reads
168+
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA && size == 0)
169+
{
170+
// extraDataRead returns 1 to let us know there's data available. Don't count it against the request body size yet.
171+
dataRead = 0;
172+
}
173+
else if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
167174
{
168175
Exception exception = new IOException(string.Empty, new HttpSysException((int)statusCode));
169176
Log.ErrorWhileRead(Logger, exception);
@@ -183,7 +190,8 @@ public override unsafe int Read([In, Out] byte[] buffer, int offset, int size)
183190

184191
internal void UpdateAfterRead(uint statusCode, uint dataRead)
185192
{
186-
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF || dataRead == 0)
193+
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF
194+
|| statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA && dataRead == 0)
187195
{
188196
Dispose();
189197
}
@@ -244,7 +252,7 @@ public override unsafe Task<int> ReadAsync(byte[] buffer, int offset, int size,
244252
cancellationRegistration = RequestContext.RegisterForCancellation(cancellationToken);
245253
}
246254

247-
asyncResult = new RequestStreamAsyncResult(this, null, null, buffer, offset, dataRead, cancellationRegistration);
255+
asyncResult = new RequestStreamAsyncResult(this, null, null, buffer, offset, size, dataRead, cancellationRegistration);
248256
uint bytesReturned;
249257

250258
try

src/Servers/HttpSys/src/RequestProcessing/RequestStreamAsyncResult.cs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,24 @@ internal unsafe class RequestStreamAsyncResult : IAsyncResult, IDisposable
1717

1818
private readonly SafeNativeOverlapped? _overlapped;
1919
private readonly IntPtr _pinnedBuffer;
20+
private readonly int _size;
2021
private readonly uint _dataAlreadyRead;
2122
private readonly TaskCompletionSource<int> _tcs;
2223
private readonly RequestStream _requestStream;
2324
private readonly AsyncCallback? _callback;
2425
private readonly CancellationTokenRegistration _cancellationRegistration;
2526

26-
internal RequestStreamAsyncResult(RequestStream requestStream, object? userState, AsyncCallback? callback)
27+
internal RequestStreamAsyncResult(RequestStream requestStream, object? userState, AsyncCallback? callback, byte[] buffer, int offset, int size, uint dataAlreadyRead, CancellationTokenRegistration cancellationRegistration)
2728
{
2829
_requestStream = requestStream;
2930
_tcs = new TaskCompletionSource<int>(userState);
3031
_callback = callback;
31-
}
32-
33-
internal RequestStreamAsyncResult(RequestStream requestStream, object? userState, AsyncCallback? callback, uint dataAlreadyRead)
34-
: this(requestStream, userState, callback)
35-
{
36-
_dataAlreadyRead = dataAlreadyRead;
37-
}
38-
39-
internal RequestStreamAsyncResult(RequestStream requestStream, object? userState, AsyncCallback? callback, byte[] buffer, int offset, uint dataAlreadyRead)
40-
: this(requestStream, userState, callback, buffer, offset, dataAlreadyRead, new CancellationTokenRegistration())
41-
{
42-
}
43-
44-
internal RequestStreamAsyncResult(RequestStream requestStream, object? userState, AsyncCallback? callback, byte[] buffer, int offset, uint dataAlreadyRead, CancellationTokenRegistration cancellationRegistration)
45-
: this(requestStream, userState, callback)
46-
{
4732
_dataAlreadyRead = dataAlreadyRead;
4833
var boundHandle = requestStream.RequestContext.Server.RequestQueue.BoundHandle;
4934
_overlapped = new SafeNativeOverlapped(boundHandle,
5035
boundHandle.AllocateNativeOverlapped(IOCallback, this, buffer));
5136
_pinnedBuffer = (Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset));
37+
_size = size;
5238
_cancellationRegistration = cancellationRegistration;
5339
}
5440

@@ -87,7 +73,13 @@ private static void IOCompleted(RequestStreamAsyncResult asyncResult, uint error
8773
{
8874
try
8975
{
90-
if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
76+
// Zero-byte reads
77+
if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA && asyncResult._size == 0)
78+
{
79+
// numBytes returns 1 to let us know there's data available. Don't count it against the request body size yet.
80+
asyncResult.Complete(0, errorCode);
81+
}
82+
else if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
9183
{
9284
asyncResult.Fail(new IOException(string.Empty, new HttpSysException((int)errorCode)));
9385
}

src/Servers/HttpSys/test/FunctionalTests/RequestBodyTests.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ public async Task RequestBody_ReadSync_Success()
3939
}
4040
}
4141

42+
[ConditionalFact]
43+
public async Task RequestBody_Read0ByteSync_Success()
44+
{
45+
string address;
46+
using (Utilities.CreateHttpServer(out address, httpContext =>
47+
{
48+
Assert.True(httpContext.Request.CanHaveBody());
49+
byte[] input = new byte[100];
50+
httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
51+
int read = httpContext.Request.Body.Read(input, 0, 0);
52+
Assert.Equal(0, read);
53+
read = httpContext.Request.Body.Read(input, 0, input.Length);
54+
httpContext.Response.ContentLength = read;
55+
httpContext.Response.Body.Write(input, 0, read);
56+
return Task.FromResult(0);
57+
}))
58+
{
59+
string response = await SendRequestAsync(address, "Hello World");
60+
Assert.Equal("Hello World", response);
61+
}
62+
}
63+
4264
[ConditionalFact]
4365
public async Task RequestBody_ReadAsync_Success()
4466
{
@@ -57,6 +79,29 @@ public async Task RequestBody_ReadAsync_Success()
5779
}
5880
}
5981

82+
[ConditionalFact]
83+
public async Task RequestBody_Read0ByteAsync_Success()
84+
{
85+
string address;
86+
using (Utilities.CreateHttpServer(out address, async httpContext =>
87+
{
88+
Assert.True(httpContext.Request.CanHaveBody());
89+
byte[] input = new byte[100];
90+
await Task.Delay(1000);
91+
int read = await httpContext.Request.Body.ReadAsync(input, 0, 1);
92+
Assert.Equal(1, read);
93+
read = await httpContext.Request.Body.ReadAsync(input, 0, 0);
94+
Assert.Equal(0, read);
95+
read = await httpContext.Request.Body.ReadAsync(input, 1, input.Length - 1);
96+
httpContext.Response.ContentLength = read + 1;
97+
await httpContext.Response.Body.WriteAsync(input, 0, read + 1);
98+
}))
99+
{
100+
string response = await SendRequestAsync(address, "Hello World");
101+
Assert.Equal("Hello World", response);
102+
}
103+
}
104+
60105
[ConditionalFact]
61106
public async Task RequestBody_ReadBeginEnd_Success()
62107
{
@@ -87,7 +132,6 @@ public async Task RequestBody_InvalidBuffer_ArgumentException()
87132
Assert.Throws<ArgumentOutOfRangeException>("offset", () => httpContext.Request.Body.Read(input, -1, 1));
88133
Assert.Throws<ArgumentOutOfRangeException>("offset", () => httpContext.Request.Body.Read(input, input.Length + 1, 1));
89134
Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 10, -1));
90-
Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 0, 0));
91135
Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 1, input.Length));
92136
Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 0, input.Length + 1));
93137
return Task.FromResult(0);

0 commit comments

Comments
 (0)