Skip to content

Commit 72bdd46

Browse files
authored
Add request feature to support buffered/bufferless streams (#170)
1 parent cdc3e15 commit 72bdd46

File tree

13 files changed

+542
-40
lines changed

13 files changed

+542
-40
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.IO;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using System.Web;
10+
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Http.Features;
12+
using Microsoft.AspNetCore.WebUtilities;
13+
14+
namespace Microsoft.AspNetCore.SystemWebAdapters;
15+
16+
internal class HttpRequestAdapterFeature : IHttpRequestAdapterFeature, IHttpRequestFeature, IDisposable
17+
{
18+
private readonly int _bufferThreshold;
19+
private readonly long? _bufferLimit;
20+
private readonly IHttpRequestFeature _other;
21+
22+
private Stream? _bufferedStream;
23+
24+
public HttpRequestAdapterFeature(IHttpRequestFeature other, int bufferThreshold, long? bufferLimit)
25+
{
26+
_bufferThreshold = bufferThreshold;
27+
_bufferLimit = bufferLimit;
28+
_other = other;
29+
}
30+
31+
public ReadEntityBodyMode Mode { get; private set; }
32+
33+
public Stream GetBufferedInputStream()
34+
{
35+
if (Mode is ReadEntityBodyMode.Buffered)
36+
{
37+
Debug.Assert(_bufferedStream is not null);
38+
return _bufferedStream;
39+
}
40+
41+
if (Mode is ReadEntityBodyMode.None)
42+
{
43+
Mode = ReadEntityBodyMode.Buffered;
44+
45+
return _bufferedStream = new FileBufferingReadStream(_other.Body, _bufferThreshold, _bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory);
46+
}
47+
48+
throw new InvalidOperationException("GetBufferlessInputStream cannot be called after other stream access");
49+
}
50+
51+
Stream IHttpRequestAdapterFeature.GetBufferlessInputStream()
52+
{
53+
if (Mode is ReadEntityBodyMode.Bufferless or ReadEntityBodyMode.None)
54+
{
55+
Mode = ReadEntityBodyMode.Bufferless;
56+
return GetBody();
57+
}
58+
59+
throw new InvalidOperationException("GetBufferlessInputStream cannot be called after other stream access");
60+
}
61+
62+
Stream IHttpRequestAdapterFeature.InputStream
63+
{
64+
get
65+
{
66+
if (Mode is ReadEntityBodyMode.Classic && _bufferedStream is not null)
67+
{
68+
return _bufferedStream;
69+
}
70+
71+
throw new InvalidOperationException("InputStream must be prebuffered");
72+
}
73+
}
74+
75+
async Task<Stream> IHttpRequestAdapterFeature.GetInputStreamAsync(CancellationToken token)
76+
{
77+
await BufferInputStreamAsync(token);
78+
return GetBody();
79+
}
80+
81+
public async Task BufferInputStreamAsync(CancellationToken token)
82+
{
83+
if (Mode is ReadEntityBodyMode.Classic)
84+
{
85+
return;
86+
}
87+
88+
if (Mode is not ReadEntityBodyMode.None)
89+
{
90+
throw new InvalidOperationException("InputStream cannot be called after other stream access");
91+
}
92+
93+
var stream = GetBufferedInputStream();
94+
await stream.DrainAsync(token);
95+
stream.Position = 0;
96+
97+
Mode = ReadEntityBodyMode.Classic;
98+
}
99+
100+
public void Dispose() => _bufferedStream?.Dispose();
101+
102+
string IHttpRequestFeature.Protocol
103+
{
104+
get => _other.Protocol;
105+
set => _other.Protocol = value;
106+
}
107+
108+
string IHttpRequestFeature.Scheme
109+
{
110+
get => _other.Scheme;
111+
set => _other.Scheme = value;
112+
}
113+
114+
string IHttpRequestFeature.Method
115+
{
116+
get => _other.Method;
117+
set => _other.Method = value;
118+
}
119+
120+
string IHttpRequestFeature.PathBase
121+
{
122+
get => _other.PathBase;
123+
set => _other.PathBase = value;
124+
}
125+
126+
string IHttpRequestFeature.Path
127+
{
128+
get => _other.Path;
129+
set => _other.Path = value;
130+
}
131+
132+
string IHttpRequestFeature.QueryString
133+
{
134+
get => _other.QueryString;
135+
set => _other.QueryString = value;
136+
}
137+
138+
string IHttpRequestFeature.RawTarget
139+
{
140+
get => _other.RawTarget;
141+
set => _other.RawTarget = value;
142+
}
143+
144+
IHeaderDictionary IHttpRequestFeature.Headers
145+
{
146+
get => _other.Headers;
147+
set => _other.Headers = value;
148+
}
149+
150+
Stream IHttpRequestFeature.Body
151+
{
152+
get
153+
{
154+
var body = GetBody();
155+
156+
if (Mode is ReadEntityBodyMode.None)
157+
{
158+
Mode = body.CanSeek ? ReadEntityBodyMode.Buffered : ReadEntityBodyMode.Bufferless;
159+
}
160+
161+
return body;
162+
}
163+
set => _other.Body = value;
164+
}
165+
166+
private Stream GetBody() => _bufferedStream ?? _other.Body;
167+
168+
internal static class AspNetCoreTempDirectory
169+
{
170+
private static string? _tempDirectory;
171+
172+
public static string TempDirectory
173+
{
174+
get
175+
{
176+
if (_tempDirectory == null)
177+
{
178+
// Look for folders in the following order.
179+
var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location.
180+
Path.GetTempPath(); // Fall back.
181+
182+
if (!Directory.Exists(temp))
183+
{
184+
throw new DirectoryNotFoundException(temp);
185+
}
186+
187+
_tempDirectory = temp;
188+
}
189+
190+
return _tempDirectory;
191+
}
192+
}
193+
194+
public static Func<string> TempDirectoryFactory => () => TempDirectory;
195+
}
196+
}

src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/PreBufferRequestStreamMiddleware.cs

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,31 @@
33

44
using System.Threading.Tasks;
55
using Microsoft.AspNetCore.Http;
6-
using Microsoft.AspNetCore.WebUtilities;
7-
using Microsoft.Extensions.Logging;
6+
using Microsoft.AspNetCore.Http.Features;
87

98
namespace Microsoft.AspNetCore.SystemWebAdapters;
109

1110
internal partial class PreBufferRequestStreamMiddleware
1211
{
1312
private readonly RequestDelegate _next;
14-
private readonly ILogger<PreBufferRequestStreamMiddleware> _logger;
13+
private readonly PreBufferRequestStreamAttribute _defaultMetadata = new() { IsDisabled = true };
1514

16-
[LoggerMessage(Level = LogLevel.Trace, Message = "Prebuffering request stream")]
17-
private partial void LogMessage();
15+
public PreBufferRequestStreamMiddleware(RequestDelegate next) => _next = next;
1816

19-
public PreBufferRequestStreamMiddleware(RequestDelegate next, ILogger<PreBufferRequestStreamMiddleware> logger)
17+
public async Task InvokeAsync(HttpContextCore context)
2018
{
21-
_next = next;
22-
_logger = logger;
23-
}
24-
25-
public Task InvokeAsync(HttpContextCore context)
26-
=> context.GetEndpoint()?.Metadata.GetMetadata<PreBufferRequestStreamAttribute>() is { IsDisabled: false } metadata
27-
? PreBufferAsync(context, metadata)
28-
: _next(context);
29-
30-
31-
private async Task PreBufferAsync(HttpContextCore context, PreBufferRequestStreamAttribute metadata)
32-
{
33-
// TODO: Should this enforce MaxRequestBodySize? https://github.com/aspnet/AspLabs/pull/447#discussion_r827314309
34-
LogMessage();
35-
36-
context.Request.EnableBuffering(metadata.BufferThreshold, metadata.BufferLimit ?? long.MaxValue);
37-
38-
await context.Request.Body.DrainAsync(context.RequestAborted);
39-
context.Request.Body.Position = 0;
19+
var metadata = context.GetEndpoint()?.Metadata.GetMetadata<PreBufferRequestStreamAttribute>() ?? _defaultMetadata;
20+
var existing = context.Features.GetRequired<IHttpRequestFeature>();
21+
var requestFeature = new HttpRequestAdapterFeature(existing, metadata.BufferThreshold, metadata.BufferLimit);
22+
23+
if(!metadata.IsDisabled)
24+
{
25+
await requestFeature.BufferInputStreamAsync(context.RequestAborted);
26+
}
27+
28+
context.Response.RegisterForDispose(requestFeature);
29+
context.Features.Set<IHttpRequestFeature>(requestFeature);
30+
context.Features.Set<IHttpRequestAdapterFeature>(requestFeature);
4031

4132
await _next(context);
4233
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#if NETCOREAPP
5+
6+
using System.IO;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using System.Web;
10+
11+
namespace Microsoft.AspNetCore.SystemWebAdapters;
12+
13+
internal interface IHttpRequestAdapterFeature
14+
{
15+
ReadEntityBodyMode Mode { get; }
16+
17+
Task<Stream> GetInputStreamAsync(CancellationToken token);
18+
19+
Stream InputStream { get; }
20+
21+
Stream GetBufferedInputStream();
22+
23+
Stream GetBufferlessInputStream();
24+
}
25+
26+
#endif

src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/IHttpResponseAdapterFeature.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
#if NET6_0_OR_GREATER
4+
#if NETCOREAPP
55

66
using System.Threading.Tasks;
77

src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,16 @@ public class HttpRequest
3636
private NameValueCollection? _params;
3737
private HttpBrowserCapabilities? _browser;
3838

39+
private FeatureReference<IHttpRequestAdapterFeature> _requestFeature;
40+
3941
internal HttpRequest(HttpRequestCore request)
4042
{
4143
_request = request;
44+
_requestFeature = FeatureReference<IHttpRequestAdapterFeature>.Default;
4245
}
4346

47+
private IHttpRequestAdapterFeature RequestFeature => _requestFeature.Fetch(_request.HttpContext.Features) ?? throw new InvalidOperationException("Please ensure you have added the System.Web adapters middleware.");
48+
4449
internal RequestHeaders TypedHeaders => _typedHeaders ??= new(_request.Headers);
4550

4651
public string? Path => _request.Path.Value;
@@ -49,6 +54,12 @@ internal HttpRequest(HttpRequestCore request)
4954

5055
public Uri Url => new(_request.GetEncodedUrl());
5156

57+
public ReadEntityBodyMode ReadEntityBodyMode => RequestFeature.Mode;
58+
59+
public Stream GetBufferlessInputStream() => RequestFeature.GetBufferlessInputStream();
60+
61+
public Stream GetBufferedInputStream() => RequestFeature.GetBufferedInputStream();
62+
5263
[SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = Constants.ApiFromAspNet)]
5364
public string? RawUrl => _request.HttpContext.Features.Get<IHttpRequestFeature>()?.RawTarget;
5465

@@ -139,9 +150,7 @@ public string? ContentType
139150
set => _request.ContentType = value;
140151
}
141152

142-
public Stream InputStream => _request.Body.CanSeek
143-
? _request.Body
144-
: throw new InvalidOperationException("Input stream must be seekable. Ensure your endpoints are either annotated with BufferRequestStreamAttribute or you've called .RequireRequestStreamBuffering() on them.");
153+
public Stream InputStream => RequestFeature.InputStream;
145154

146155
public NameValueCollection ServerVariables => _serverVariables ??= _request.HttpContext.Features.GetRequired<IServerVariablesFeature>().ToNameValueCollection();
147156

@@ -268,7 +277,7 @@ public void SaveAs(string filename, bool includeHeaders)
268277
w.WriteLine();
269278
}
270279

271-
WriteTo(InputStream, f);
280+
WriteTo(GetBufferedInputStream(), f);
272281
}
273282

274283
/// <summary>

src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestBase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public virtual string? ContentType
5252

5353
public virtual Stream InputStream => throw new NotImplementedException();
5454

55+
public virtual Stream GetBufferedInputStream() => throw new NotImplementedException();
56+
57+
public virtual Stream GetBufferlessInputStream() => throw new NotImplementedException();
58+
5559
public virtual NameValueCollection ServerVariables => throw new NotImplementedException();
5660

5761
public virtual bool IsSecureConnection => throw new NotImplementedException();

src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequestWrapper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ public override string? ContentType
4141

4242
public override Stream InputStream => _request.InputStream;
4343

44+
public override Stream GetBufferedInputStream() => _request.GetBufferedInputStream();
45+
46+
public override Stream GetBufferlessInputStream() => _request.GetBufferlessInputStream();
47+
4448
public override bool IsAuthenticated => _request.IsAuthenticated;
4549

4650
public override bool IsLocal => _request.IsLocal;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Web
5+
{
6+
public enum ReadEntityBodyMode
7+
{
8+
None,
9+
Classic, // BinaryRead, Form, Files, InputStream
10+
Bufferless, // GetBufferlessInputStream
11+
Buffered // GetBufferedInputStream
12+
}
13+
}

0 commit comments

Comments
 (0)