No Spring, uma requisição não vai direto para o seu Controller

Quando uma aplicação Spring recebe uma chamada HTTP, é comum imaginar um caminho simples:

requisição entrou, controller executou, resposta voltou.

Essa leitura ajuda no começo.

Mas ela é incompleta.

Antes de um método anotado com @GetMapping, @PostMapping ou @PutMapping ser chamado, muita coisa já aconteceu.

A requisição passou pelo servidor web, atravessou filtros, chegou ao DispatcherServlet, foi comparada com os mapeamentos disponíveis, teve argumentos resolvidos, corpo convertido e validações aplicadas.

Só depois disso o seu método no controller entra em cena.

Entender esse caminho muda bastante a forma de depurar erros em aplicações Spring.

Às vezes o controller não foi chamado porque o problema aconteceu antes dele.


O Controller não é a porta mais externa

Um @RestController parece a entrada da aplicação porque é ali que você escreve o endpoint:

@RestController
@RequestMapping("/pedidos")
public class PedidoController {

    @PostMapping
    public ResponseEntity<PedidoResponse> criar(
        @RequestBody @Valid CriarPedidoRequest request
    ) {
        // regra delegada para o caso de uso
        return ResponseEntity.ok().build();
    }
}

Esse método representa uma rota HTTP.

Mas ele não recebe a conexão diretamente.

Quem recebe a requisição primeiro é a infraestrutura web da aplicação.

Em uma aplicação Spring Boot comum, isso envolve um servidor embutido, como Tomcat, Jetty ou Undertow.

Esse servidor recebe a conexão, transforma a chamada em objetos HTTP da plataforma Java e entrega o fluxo para a cadeia configurada na aplicação.

O controller está dentro desse fluxo.

Ele não está no começo dele.


Antes do MVC, existem filtros

Um ponto importante é a cadeia de Filter.

Filtros atuam antes de a requisição chegar ao processamento MVC do Spring.

Eles podem registrar logs, criar correlation id, tratar CORS, aplicar compressão, validar autenticação, bloquear chamadas ou alterar informações da requisição.

Um filtro pode impedir que o controller seja chamado:

@Component
public class ApiKeyFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        String apiKey = request.getHeader("X-Api-Key");

        if (apiKey == null || apiKey.isBlank()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        filterChain.doFilter(request, response);
    }
}

Se o header não existir, a resposta termina ali.

Nenhum @PostMapping será executado.

Isso explica uma situação comum: você coloca um breakpoint no controller, faz a chamada e nada acontece.

O endpoint pode estar correto.

O problema pode estar antes dele.

Em aplicações com Spring Security, essa ideia fica ainda mais importante.

Boa parte das decisões de autenticação e autorização acontece na cadeia de filtros.

Muitas respostas 401 e 403 nascem antes do método do controller.


O DispatcherServlet é o centro do Spring MVC

Depois dos filtros, a requisição chega ao DispatcherServlet.

Ele é uma peça central do Spring MVC.

O papel dele não é executar sua regra de negócio.

O papel dele é coordenar o processamento web.

De forma simplificada, ele precisa descobrir:

  • qual controller deve atender a requisição;
  • qual método combina com o caminho e o verbo HTTP;
  • quais argumentos devem ser montados;
  • quais conversores devem ler ou escrever o corpo;
  • quais interceptors participam da chamada;
  • como transformar exceções em resposta HTTP.

O nome DispatcherServlet é bem literal.

Ele despacha a requisição para o handler adequado.

O controller é o handler mais visível para quem escreve código de aplicação, mas ele é apenas uma parte da engrenagem.


O mapeamento pode falhar antes do método

Antes de chamar o controller, o Spring precisa encontrar um método compatível.

Esse trabalho passa por estruturas como HandlerMapping.

Elas comparam a requisição recebida com as rotas registradas na aplicação.

Não basta o caminho parecer parecido.

O Spring considera elementos como:

  • path;
  • verbo HTTP;
  • headers exigidos;
  • parâmetros;
  • consumes;
  • produces.

Por isso um método como este não atende qualquer POST /pedidos:

@PostMapping(
    path = "/pedidos",
    consumes = "application/json",
    produces = "application/json"
)
public PedidoResponse criar(@RequestBody CriarPedidoRequest request) {
    return new PedidoResponse();
}

Se a chamada vier com Content-Type incompatível, o método pode nem ser selecionado.

Se o verbo estiver errado, o resultado pode ser 405.

Se a rota não existir, pode ser 404.

Nada disso exige que o corpo do método seja executado.

O erro pode estar no contrato HTTP antes de estar na lógica do controller.


Argumentos também são montados antes

Quando o método certo é encontrado, o Spring ainda precisa montar os parâmetros.

Um controller raramente recebe apenas strings cruas.

Ele recebe objetos, IDs convertidos, headers, query params, path variables e corpos JSON transformados em classes Java.

@GetMapping("/pedidos/{id}")
public PedidoResponse buscar(
    @PathVariable Long id,
    @RequestHeader("X-Correlation-Id") String correlationId
) {
    return new PedidoResponse();
}

Antes de executar buscar, o Spring precisa extrair {id} da URL e converter para Long.

Também precisa encontrar o header exigido.

Se o valor não puder ser convertido ou o header obrigatório não existir, a chamada pode falhar antes da primeira linha do método.

Com @RequestBody, acontece algo parecido:

@PostMapping("/pedidos")
public PedidoResponse criar(@RequestBody CriarPedidoRequest request) {
    return new PedidoResponse();
}

O JSON precisa ser lido, interpretado e convertido para CriarPedidoRequest.

Se o corpo estiver malformado, o controller não recebe um objeto parcialmente mágico.

A conversão falha antes.


Validação também pode barrar a chamada

Quando o parâmetro usa @Valid, existe mais uma etapa antes da execução do método:

public record CriarPedidoRequest(
    @NotNull Long clienteId,
    @NotEmpty List<ItemRequest> itens
) {
}

@PostMapping("/pedidos")
public PedidoResponse criar(@RequestBody @Valid CriarPedidoRequest request) {
    return new PedidoResponse();
}

Aqui o Spring converte o corpo da requisição e depois dispara a validação.

Se clienteId vier nulo ou itens vier vazio, o método pode não ser executado.

Isso é bom.

O controller não deveria precisar verificar manualmente tudo que pertence ao contrato de entrada.

Mas, para depurar corretamente, você precisa lembrar que a validação acontece antes do corpo do método.

Se o breakpoint não parou, talvez a entrada tenha sido recusada na montagem do argumento.


Filter e Interceptor não são a mesma coisa

Outro ponto que gera confusão é misturar Filter com HandlerInterceptor.

Os dois podem atuar em requisições HTTP, mas entram em momentos diferentes.

O Filter faz parte da cadeia Servlet e roda antes do processamento do Spring MVC.

O HandlerInterceptor participa depois que o Spring MVC já começou a processar a requisição e normalmente depois que um handler foi identificado.

@Component
public class CorrelationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler
    ) {
        return true;
    }
}

Um interceptor pode executar lógica antes do controller, depois dele ou após a conclusão da requisição.

Ele é útil quando a lógica depende do contexto MVC.

Um filtro é melhor quando a regra precisa acontecer mais cedo, de forma mais externa, antes do Spring decidir qual handler será usado.

Essa diferença importa em logs, segurança, métricas, auditoria e tratamento de erro.

Colocar uma lógica no ponto errado pode fazer ela rodar cedo demais, tarde demais ou nem rodar para certos tipos de falha.


Por que isso importa no dia a dia

Quando você entende que a requisição não vai direto para o controller, alguns sintomas ficam mais fáceis de interpretar.

Um 401 pode ter nascido no filtro de segurança.

Um 404 pode indicar que nenhum mapeamento combinou com a requisição.

Um 405 pode ser verbo HTTP errado.

Um 415 pode ser Content-Type incompatível.

Um 400 pode ter vindo de JSON inválido, conversão de parâmetro ou validação.

Um erro tratado por @ControllerAdvice pode ter sido lançado durante a montagem dos argumentos, antes de qualquer regra do endpoint.

O controller continua sendo importante.

Mas ele não é o único lugar para procurar a causa.


O ponto que vale fixar

No Spring MVC, o controller não é a primeira parada da requisição.

Ele é o ponto em que a requisição chega depois de passar por uma sequência de decisões técnicas.

Servidor, filtros, DispatcherServlet, mapeamento, conversores, validação e interceptors fazem parte do caminho.

Quando você entende essa ordem, deixa de tratar todo problema HTTP como bug no controller.

Às vezes o método não executou porque o Spring protegeu a entrada antes.

E essa é exatamente uma das funções do framework.