Introdução
Quando o ASP.NET Core está sendo executado em um AWS Lambda e recebendo solicitações através de um AWS API Gateway, o aplicativo não é notificado de um tempo limite do API Gateway e continua processando a solicitação, concluindo-a eventualmente. Isso deixará métricas e logs de uma solicitação bem-sucedida quando o cliente recebeu um erro de tempo limite.

Neste post, mostrarei como resolver este problema com tokens de cancelamento e tempos limite.
A solução
A ideia é definir o tempo limite da solicitação no aplicativo ASP.NET antes do tempo limite do API Gateway. Desta forma, podemos coletar logs e métricas das solicitações que atingiram o tempo limite e ter uma visão realista dos erros que os usuários estão vendo.

Neste post anterior, expliquei como cancelar solicitações abortadas pelo cliente HTTP usando o CancellationToken
nos métodos dos controladores.
O model binding padrão do ASP.NET para o CancellationToken
injeta a propriedade RequestAborted
do objeto HttpContext
nos métodos do controlador. A propriedade RequestAborted
contém um CancellationToken
que é acionado quando o cliente abandona a solicitação.
Vamos criar um middleware para aplicar o tempo limite ao Cancellation Token do ASP.NET Core.
Primeiro, criaremos um novo CancellationTokenSource
e definiremos um tempo limite para ele.
Em seguida, usaremos o método CancellationToken.CreateLinkedTokenSource
para vincular o CancellationToken HttpContext.RequestAborted
ao novo CancellationToken que criamos com um tempo limite.
Por último, substituímos o Cancellation Token HttpContext.RequestAborted
pelo token retornado pelo CreateLinkedTokenSource.
⚠️ Lembre-se de sempre descartar os objetos CancellationTokenSource
para evitar vazamentos de memória. Use a palavra-chave using
ou descarte-os no delegado HttpContext.Response.OnCompleted
.
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
| public class TimeoutCancellationMiddleware
{
private readonly RequestDelegate _next;
private readonly TimeSpan _timeout;
public TimeoutCancellationMiddleware(RequestDelegate next, TimeoutCancellationMiddlewareOptions options)
{
_next = next;
_timeout = options.Timeout;
}
public async Task InvokeAsync(HttpContext context)
{
//Cria um novo CancellationTokenSource e define um tempo limite
using var timeoutCancellationTokenSource = new CancellationTokenSource();
timeoutCancellationTokenSource.CancelAfter(_timeout);
//Cria um novo CancellationTokenSource vinculando o timeoutCancellationToken e o CancellationToken RequestAborted do ASP.NET
using var combinedCancellationTokenSource =
CancellationTokenSource
.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, context.RequestAborted);
//Substitui o CancellationToken RequestAborted pelo nosso CancellationToken combinado
context.RequestAborted = combinedCancellationTokenSource.Token;
await _next(context);
}
}
public class TimeoutCancellationMiddlewareOptions
{
public TimeSpan Timeout { get; set; }
public TimeoutCancellationMiddlewareOptions(TimeSpan timeout)
{
Timeout = timeout;
}
}
|
Também podemos criar um método de extensão para facilitar a configuração do middleware:
1
2
3
4
5
6
7
8
9
| public static class TimeoutCancellationMiddlewareExtensions
{
public static IApplicationBuilder UseTimeoutCancellationToken(
this IApplicationBuilder builder, TimeSpan timeout)
{
return builder.UseMiddleware<TimeoutCancellationMiddleware>(
new TimeoutCancellationMiddlewareOptions(timeout));
}
}
|
Em seguida, adicione-o ao pipeline de middleware em nosso aplicativo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
...
//Configura um tempo limite de solicitação de 10 segundos
app.UseTimeoutCancellationToken(TimeSpan.FromSeconds(10));
...
app.Run();
}
}
|
Agora, temos que usar o CancellationToken
injetado nos métodos dos controladores e passá-lo para todos os métodos assíncronos.
DogImageController:
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
| [ApiController]
[Route("[controller]")]
public class DogImageController : ControllerBase
{
private readonly IDogApi _dogApi;
private readonly ILogger<DogImageController> _logger;
public DogImageController(IDogApi dogApi, ILogger<DogImageController> logger)
{
_dogApi = dogApi;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<string>> GetAsync(CancellationToken cancellationToken)
{
try
{
var dog = await _dogApi.GetRandomDog(cancellationToken);
return dog.message;
}
catch(TaskCanceledException ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError, "Request timed out or canceled");
}
}
}
|
🚨 Não capture TaskCanceledException
em nenhum lugar, exceto no controlador, ou sua API pode retornar um resultado 200
/ OK
vazio.
DogImageUseCase:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class DogImageUseCase: IDogImageUseCase
{
private readonly IDogApi _dogApi;
public DogImageUseCase(IDogApi dogApi)
{
_dogApi = dogApi;
}
public async Task<string> GetRandomDogImage(CancellationToken cancellationToken)
{
var dog = await _dogApi.GetRandomDog(cancellationToken);
return dog.message;
}
}
|
Testando a solução
Para testar se funciona, vamos forçar um atraso na classe DogImageUseCase
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class DogImageUseCase: IDogImageUseCase
{
private readonly IDogApi _dogApi;
public DogImageUseCase(IDogApi dogApi)
{
_dogApi = dogApi;
}
public async Task<string> GetRandomDogImage(CancellationToken cancellationToken)
{
var dog = await _dogApi.GetRandomDog(cancellationToken);
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
return dog.message;
}
}
|

Criando um teste de integração para o cenário de tempo limite
Neste post anterior, expliquei como usar WireMock.Net
para simular dependências de API em testes de integração.
Agora, usaremos WireMock.Net
para criar um mock que responde com um atraso. Desta forma, podemos validar se nosso aplicativo está parando o processamento da solicitação quando o tempo limite ocorre.
Primeiro, substituímos o valor de configuração HttpClientTimeoutSeconds
para um tempo limite maior que o tempo limite da nossa solicitação. Esta configuração é usada para definir a propriedade Timeout
do HttpClient
. Se o tempo limite do HttpClient for menor que o tempo limite da nossa solicitação, ele abortará a solicitação para as dependências antes que o aplicativo atinja o tempo limite.
Em seguida, usamos o AddGlobalProcessingDelay
do WireMock para inserir um atraso no mock da API e forçar nosso aplicativo a atingir o tempo limite antes da resposta.
Por último, afirmamos que nosso aplicativo retornou o código de status 500
com a mensagem Request timed out or canceled
.
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
| public class DogImageTests: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public DogImageTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
...
[Fact]
public async Task Timeout_Returns_500WithMessage()
{
//Arrange
var wireMockSvr = WireMockServer.Start();
var factory = _factory
.WithWebHostBuilder(builder =>
{
builder.UseSetting("DogApiUrl", wireMockSvr.Url);
//Define o tempo limite do HttpClient para 30, para que não seja acionado antes do nosso tempo limite de solicitação que é de 10 segundos
builder.UseSetting("HttpClientTimeoutSeconds", "30");
//Define o tempo limite da solicitação para 5 segundos para que o teste seja executado mais rapidamente
builder.UseSetting("RequestTimeoutSeconds", "5");
});
var httpClient = factory.CreateClient();
Fixture fixture = new Fixture();
var responseObj = fixture.Create<Dog>();
var responseObjJson = JsonSerializer.Serialize(responseObj);
wireMockSvr
.Given(Request.Create()
.WithPath("/breeds/image/random")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(responseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
//Adiciona um atraso à resposta para causar um tempo limite de solicitação no endpoint /DogImage
wireMockSvr.AddGlobalProcessingDelay(TimeSpan.FromSeconds(15));
//Act
var apiHttpResponse = await httpClient.GetAsync("/DogImage");
//Assert
apiHttpResponse.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
var responseMessage = await apiHttpResponse.Content.ReadAsStringAsync();
responseMessage.Should().BeEquivalentTo("Request timed out or canceled");
wireMockSvr.Stop();
}
}
|
Código completo
Verifique no Repositório GitHub.
Referências e links