Skip to content

Commit 0f07743

Browse files
authored
configure MaxRequestBodySize via environment variable (#7229)
1 parent 5367f3e commit 0f07743

File tree

6 files changed

+221
-2
lines changed

6 files changed

+221
-2
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Http.Features;
9+
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Middleware
12+
{
13+
/// <summary>
14+
/// A middleware responsible for MaxRequestBodySize size configuration
15+
/// </summary>
16+
internal class HttpRequestBodySizeMiddleware
17+
{
18+
private readonly RequestDelegate _next;
19+
private readonly IEnvironment _environment;
20+
private RequestDelegate _invoke;
21+
private long _maxRequestBodySize;
22+
23+
public HttpRequestBodySizeMiddleware(RequestDelegate next, IEnvironment environment)
24+
{
25+
_next = next ?? throw new ArgumentNullException(nameof(next));
26+
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
27+
_invoke = InvokeBeforeSpecialization;
28+
}
29+
30+
// for testing
31+
internal RequestDelegate InnerInvoke { get => _invoke; }
32+
33+
public Task Invoke(HttpContext context)
34+
{
35+
return _invoke(context);
36+
}
37+
38+
internal Task InvokeAfterSpecialization(HttpContext httpContext)
39+
{
40+
var httpBodySizeFeature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
41+
if (httpBodySizeFeature == null)
42+
{
43+
throw new InvalidOperationException("Unable to Configure MaxRequestBodySize. IHttpMaxRequestBodySizeFeature is not present");
44+
}
45+
46+
httpBodySizeFeature.MaxRequestBodySize = _maxRequestBodySize;
47+
return _next.Invoke(httpContext);
48+
}
49+
50+
internal Task InvokeBeforeSpecialization(HttpContext context)
51+
{
52+
if (!_environment.IsPlaceholderModeEnabled())
53+
{
54+
string bodySizeLimit = _environment.GetEnvironmentVariable(FunctionsRequestBodySizeLimit);
55+
if (long.TryParse(bodySizeLimit, out _maxRequestBodySize))
56+
{
57+
Interlocked.Exchange(ref _invoke, InvokeAfterSpecialization);
58+
return Invoke(context);
59+
}
60+
else
61+
{
62+
Interlocked.Exchange(ref _invoke, _next);
63+
}
64+
}
65+
return _next(context);
66+
}
67+
}
68+
}

src/WebJobs.Script.WebHost/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args = null)
4040
return AspNetCore.WebHost.CreateDefaultBuilder(args)
4141
.ConfigureKestrel(o =>
4242
{
43-
o.Limits.MaxRequestBodySize = 104857600;
43+
o.Limits.MaxRequestBodySize = ScriptConstants.DefaultMaxRequestBodySize;
4444
})
4545
.UseSetting(WebHostDefaults.EnvironmentKey, Environment.GetEnvironmentVariable(EnvironmentSettingNames.EnvironmentNameKey))
4646
.ConfigureServices(services =>
4747
{
4848
services.Configure<IISServerOptions>(o =>
4949
{
50-
o.MaxRequestBodySize = 104857600;
50+
o.MaxRequestBodySize = ScriptConstants.DefaultMaxRequestBodySize;
5151
});
5252
services.Replace(ServiceDescriptor.Singleton<IServiceProviderFactory<IServiceCollection>>(new WebHostServiceProviderFactory()));
5353
})

src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder
2727
IOptionsMonitor<HttpBodyControlOptions> httpBodyControlOptions = builder.ApplicationServices.GetService<IOptionsMonitor<HttpBodyControlOptions>>();
2828
IServiceProvider serviceProvider = builder.ApplicationServices;
2929

30+
builder.UseMiddleware<HttpRequestBodySizeMiddleware>();
3031
builder.UseMiddleware<SystemTraceMiddleware>();
3132
builder.UseMiddleware<HostnameFixupMiddleware>();
3233
if (environment.IsLinuxConsumption())

src/WebJobs.Script/Environment/EnvironmentSettingNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public static class EnvironmentSettingNames
5858
public const string HealthPingEnabled = "WEBSITE_FUNCTIONS_HEALTH_PING_ENABLED";
5959
public const string TestDataCapEnabled = "WEBSITE_FUNCTIONS_TESTDATA_CAP_ENABLED";
6060
public const string AzureMonitorCategories = "WEBSITE_FUNCTIONS_AZUREMONITOR_CATEGORIES";
61+
public const string FunctionsRequestBodySizeLimit = "FUNCTIONS_REQUEST_BODY_SIZE_LIMIT";
6162

6263
//Function in Kubernetes
6364
public const string PodNamespace = "POD_NAMESPACE";

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ public static class ScriptConstants
184184
public static readonly ImmutableArray<string> AssemblyFileTypes = ImmutableArray.Create(".dll", ".exe");
185185
public static readonly string HostUserAgent = $"azure-functions-host/{ScriptHost.Version}";
186186
public static readonly NuGetVersion ExtensionBundleVersionTwo = new NuGetVersion("2.0.0");
187+
public static readonly long DefaultMaxRequestBodySize = 104857600;
187188

188189
public static readonly ImmutableArray<string> SystemLogCategoryPrefixes = ImmutableArray.Create("Microsoft.Azure.WebJobs.", "Function.", "Worker.", "Host.");
189190
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Builder;
9+
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Http.Features;
12+
using Microsoft.AspNetCore.TestHost;
13+
using Microsoft.Azure.WebJobs.Script.WebHost.Middleware;
14+
using Microsoft.Extensions.DependencyInjection;
15+
using Xunit;
16+
17+
namespace Microsoft.Azure.WebJobs.Script.Tests.Middleware
18+
{
19+
public class HttpRequestBodySizeMiddlewareTests
20+
{
21+
[Theory]
22+
[InlineData("", false)]
23+
[InlineData(null, false)]
24+
[InlineData("invalid", false)]
25+
[InlineData("1024", true)]
26+
public async Task MaxRequestBodySizeLimit_RequestBodySizeLimit_SetExpectedValue(string requestBodySizeLimit, bool validData)
27+
{
28+
var environment = new TestEnvironment();
29+
environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionsRequestBodySizeLimit, requestBodySizeLimit);
30+
environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0");
31+
32+
bool nextInvoked = false;
33+
long? configuredLimit = 0;
34+
RequestDelegate next = (ctxt) =>
35+
{
36+
nextInvoked = true;
37+
ctxt.Response.StatusCode = (int)HttpStatusCode.Accepted;
38+
configuredLimit = ctxt.Features.Get<IHttpMaxRequestBodySizeFeature>().MaxRequestBodySize;
39+
return Task.CompletedTask;
40+
};
41+
42+
var middleware = new HttpRequestBodySizeMiddleware(next, environment);
43+
44+
var httpContext = new DefaultHttpContext();
45+
46+
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(new TestHttpMaxRequestBodySizeFeature());
47+
await middleware.Invoke(httpContext);
48+
Assert.True(nextInvoked);
49+
50+
if (validData)
51+
{
52+
Assert.Equal(configuredLimit, long.Parse(requestBodySizeLimit));
53+
}
54+
else
55+
{
56+
Assert.Equal(configuredLimit, ScriptConstants.DefaultMaxRequestBodySize);
57+
}
58+
}
59+
60+
[Theory]
61+
[InlineData("")]
62+
[InlineData(null)]
63+
[InlineData("invalid")]
64+
[InlineData("1024")]
65+
public async Task MaxRequestBodySizeLimit_InPlaceHolderMode_DoesNotUpdateConfiguration(string requestBodySizeLimit)
66+
{
67+
var environment = new TestEnvironment();
68+
environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionsRequestBodySizeLimit, requestBodySizeLimit);
69+
environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1");
70+
71+
bool nextInvoked = false;
72+
long? configuredLimit = 0;
73+
RequestDelegate next = (ctxt) =>
74+
{
75+
nextInvoked = true;
76+
ctxt.Response.StatusCode = (int)HttpStatusCode.Accepted;
77+
configuredLimit = ctxt.Features.Get<IHttpMaxRequestBodySizeFeature>().MaxRequestBodySize;
78+
return Task.CompletedTask;
79+
};
80+
81+
var middleware = new HttpRequestBodySizeMiddleware(next, environment);
82+
83+
var httpContext = new DefaultHttpContext();
84+
85+
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(new TestHttpMaxRequestBodySizeFeature());
86+
await middleware.Invoke(httpContext);
87+
Assert.True(nextInvoked);
88+
Assert.Equal(configuredLimit, ScriptConstants.DefaultMaxRequestBodySize);
89+
}
90+
91+
[Theory]
92+
[InlineData(true, true)]
93+
[InlineData(true, false)]
94+
[InlineData(false, false)]
95+
[InlineData(false, true)]
96+
public async Task MaxRequestBodySizeLimit_MiddleWareInvoke_GetWiredUpCorrectly(bool inPlaceHolderMode, bool isLimitSet)
97+
{
98+
bool nextInvoked = false;
99+
long? configuredLimit = 0;
100+
RequestDelegate next = (ctxt) =>
101+
{
102+
nextInvoked = true;
103+
ctxt.Response.StatusCode = (int)HttpStatusCode.Accepted;
104+
configuredLimit = ctxt.Features.Get<IHttpMaxRequestBodySizeFeature>().MaxRequestBodySize;
105+
return Task.CompletedTask;
106+
};
107+
108+
var environment = new TestEnvironment();
109+
110+
string placeHolderModeValue = inPlaceHolderMode ? "1" : "0";
111+
string limit = isLimitSet ? "100" : "invalid";
112+
113+
environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, placeHolderModeValue);
114+
environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionsRequestBodySizeLimit, limit);
115+
116+
var middleware = new HttpRequestBodySizeMiddleware(next, environment);
117+
var httpContext = new DefaultHttpContext();
118+
119+
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(new TestHttpMaxRequestBodySizeFeature());
120+
await middleware.Invoke(httpContext);
121+
Assert.True(nextInvoked);
122+
123+
if (inPlaceHolderMode)
124+
{
125+
Assert.Equal(middleware.InnerInvoke, middleware.InvokeBeforeSpecialization);
126+
}
127+
128+
if (!inPlaceHolderMode && isLimitSet)
129+
{
130+
Assert.Equal(middleware.InnerInvoke, middleware.InvokeAfterSpecialization);
131+
}
132+
133+
if (!inPlaceHolderMode && !isLimitSet)
134+
{
135+
Assert.Equal(middleware.InnerInvoke, next);
136+
}
137+
}
138+
139+
public class TestHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
140+
{
141+
private long? _maxRequestBodySize = ScriptConstants.DefaultMaxRequestBodySize;
142+
143+
public bool IsReadOnly => false;
144+
145+
long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize { get => _maxRequestBodySize; set => _maxRequestBodySize = value; }
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)