-
Notifications
You must be signed in to change notification settings - Fork 3
/
ThrottlingTrollMiddleware.cs
104 lines (86 loc) · 4 KB
/
ThrottlingTrollMiddleware.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
[assembly: InternalsVisibleTo("ThrottlingTroll.AzureFunctionsAspNet.Tests")]
namespace ThrottlingTroll
{
/// <summary>
/// Implements ingress throttling for Azure Functions with ASP.NET Core integration.
/// </summary>
public class ThrottlingTrollMiddleware : ThrottlingTroll
{
internal ThrottlingTrollMiddleware
(
ThrottlingTrollOptions options
) : base(options.Log, options.CounterStore, options.GetConfigFunc, options.IdentityIdExtractor, options.CostExtractor, options.IntervalToReloadConfigInSeconds)
{
this._responseFabric = options.ResponseFabric;
}
/// <summary>
/// Is invoked by Azure Functions middleware pipeline. Handles ingress throttling.
/// </summary>
public async Task Invoke(FunctionContext functionContext, Func<Task> next)
{
var context = functionContext.GetHttpContext();
var requestProxy = new IncomingHttpRequestProxy(functionContext);
var cleanupRoutines = new List<Func<Task>>();
try
{
// Need to call the rest of the pipeline no more than one time
var callNextOnce = ThrottlingTrollCoreExtensions.RunOnce(() => next());
var checkList = await this.IsIngressOrEgressExceededAsync(requestProxy, cleanupRoutines, callNextOnce);
await this.ConstructResponse(context, checkList, requestProxy, callNextOnce);
}
finally
{
await Task.WhenAll(cleanupRoutines.Select(f => f()));
}
}
private readonly Func<List<LimitCheckResult>, IHttpRequestProxy, IHttpResponseProxy, CancellationToken, Task> _responseFabric;
private async Task ConstructResponse(HttpContext context, List<LimitCheckResult> checkList, IHttpRequestProxy requestProxy, Func<Task> callNextOnce)
{
var result = checkList
.Where(r => r.RequestsRemaining < 0)
// Sorting by the suggested RetryAfter header value (which is expected to be in seconds) in descending order
.OrderByDescending(r => r.RetryAfterInSeconds)
.FirstOrDefault();
if (result == null)
{
return;
}
// Exceeded rule's ResponseFabric takes precedence.
// But 1) it can be null and 2) result.Rule can also be null (when 429 is propagated from egress)
var responseFabric = result.Rule?.ResponseFabric ?? this._responseFabric;
if (responseFabric == null)
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
// Formatting default Retry-After response
if (!string.IsNullOrEmpty(result.RetryAfterHeaderValue))
{
context.Response.Headers.Add(HeaderNames.RetryAfter, result.RetryAfterHeaderValue);
}
string responseString = DateTime.TryParse(result.RetryAfterHeaderValue, out var dt) ?
result.RetryAfterHeaderValue :
$"{result.RetryAfterHeaderValue} seconds";
await context.Response.WriteAsync($"Retry after {responseString}");
}
else
{
// Using the provided response builder
var responseProxy = new IngressHttpResponseProxy(context.Response);
await responseFabric(checkList, requestProxy, responseProxy, context.RequestAborted);
if (responseProxy.ShouldContinueAsNormal)
{
// Continue with normal request processing
await callNextOnce();
}
}
}
}
}