Um erro imperdoável, na implementação de microsserviços é considerar que a conexão é estável e confiável. Por razões variadas, a rede pode falhar tanto ao enviar uma requisição quanto ao trazer uma resposta.
No lado cliente (microsserviço que está fazendo uma requisição) é fundamental implementar:
- estratégias de proteção ao servidor, como a implementação do padrão Circuit Breaker
- estretégias de repetição de requisição, como a implementação do padrão Retry
No lado servidor (microsserviço que está atendendo uma requisição) é indispensável garantir que todas as requisições sejam idempotentes. Em cenários onde isso não for absolutamente possível, é fundamental garantir que o “estado” do servidor não seja comprometido.
Se tiver interesse em entender mais sobre microsserviços, recomendo que acesse o Guia de Conteúdo para Microsserviços deste site.
O problema com POST
O verbo POST é, por definição, não idempotente.
Considere o seguinte cenário:
MOMENTO 1 – Uma aplicação cliente chama um microsserviço para realizar a inclusão de uma informação. Esta chamada está disponível através de uma operação POST.
MOMENTO 2 – A aplicação servidor recebe a requisição e executa o processamento de forma adequada. Entretanto, a mensagem de resposta acaba se perdendo na rede e, por alguma razão, não chega a aplicação cliente.
MOMENTO 3 – Depois de algum tempo a aplicação cliente identifica um time-out. E, por definição, faz uma nova tentativa de inclusão de informação.
MOMENTO 4 – A aplicação servidor recebe a nova requisição e, inadvertidamente, inclui a mesma informação, uma segunda vez, na base.
Assustador, não acha? Esse é um caso extremamente difícil de detectar e raramente é “pego” em testes de QA.
Uma possível solução
De forma radical, o cenário acima me faz evitar utilizar o verbo post em minhas aplicações. Acabo utilizando um “PUT identificado” (essa solução, aliás vem sendo bastante aplicada estando presente, inclusive em uma aplicação de referência da Microsoft).
[HttpPut]
[ProducesResponseType(typeof(string), (int) HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task GetEcho(
[FromBody] EchoCommand command,
[FromHeader(Name = "x-requestid")] string requestId
)
{
if (!Guid.TryParse(requestId, out var guid))
{
return BadRequest();
}
var identifiedCommand = new IdentifiedCommand<EchoCommand, string>(
command,
guid
);
return Ok(await _mediator.Send(identifiedCommand));
}
O caminho que venho adotando é pedir, em operações que mudam o estado do servidor, um identificador da requisição. Este identificador é então ligado ao comando.
A aplicação cliente precisará adicionar um header para identificar a requisição.

Um handler global para comandos identificados checa se o identificador está associado a um comando já executado. Caso esteja, gera um erro.
public class IdentifiedCommandHandler<TCommand, TResult>
: IRequestHandler<IdentifiedCommand<TCommand, TResult>, TResult>
where TCommand : IRequest
{
private readonly IMediator _mediator;
private readonly IRequestManager _requestManager;
public IdentifiedCommandHandler(IMediator mediator, IRequestManager requestManager)
{
_mediator = mediator;
_requestManager = requestManager;
}
public async Task Handle(
IdentifiedCommand<TCommand, TResult> message,
CancellationToken cancellationToken = default(CancellationToken)
)
{
if (message.Id == Guid.Empty)
{
ThrowMediatrPipelineException.IdentifiedCommandWithoutId();
}
if (message.Command == null)
{
ThrowMediatrPipelineException.IdentifiedCommandWithoutInnerCommand();
}
var alreadyRegistered = await _requestManager.IsRegistered(message.Id, cancellationToken);
if (alreadyRegistered)
{
ThrowMediatrPipelineException.CommandWasAlreadyExecuted();
}
await _requestManager.Register(message.Id, cancellationToken);
var result = await _mediator.Send(message.Command, cancellationToken);
return result;
}
}
Todas as exceptions relacionadas a uma falha por duplicidade são mapeadas adequadamente para um Bad Request.
public class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IHostingEnvironment _env;
private readonly ILogger _logger;
public HttpGlobalExceptionFilter(
IHostingEnvironment env,
ILogger logger
)
{
_env = env;
_logger = logger;
}
public void OnException(ExceptionContext context)
{
_logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(MediatrPipelineException))
{
var validationException = context.Exception.InnerException as ValidationException;
if (validationException != null)
{
var json = new JsonErrorResponse
{
Messages = validationException.Errors
.Select(e => e.ErrorMessage)
.ToArray()
};
context.Result = new BadRequestObjectResult(json);
}
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
var json = new JsonErrorResponse
{
Messages = new[]
{
"Internal Error. Try again later.",
context.Exception.GetType().ToString(),
context.Exception.Message
}
};
context.Result = new ObjectResult(json) { StatusCode = 500 };
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
}
public class JsonErrorResponse
{
public string[] Messages { get; set; }
}
}
Que geram um Bad Request para o cliente.

Concluindo
A solução perfeita entregaria idempotência. Mas, para isso, eu precisaria armazenar a resposta do servidor e isso poderia conduzir a erros de interpretação. Aqui, opto por deixar claro o que houve de errado.
Mais uma vez, é importante ressaltar que, implementando microsserviços, é fundamental estar preparado para instabilidades de conexão. A questão não é se ocorrerá uma falha, mas quando essa falha irá ocorrer.
O código fonte desse exemplo está disponível no github.
Créditos da imagem da capa para Park Troopers

