Requestmapping

@RequestMapping

Glossary

Wilcard doble = **

wildcard individual o simple = *

RFD = reflected file download (attack)

@RequestMapping

Puedes usar la anotación @RequestMapping para mapear solicitudes a métodos de controladores. Tiene varios atributos para hacer coincidencias por URL, método HTTP, parámetros de solicitud, headers y tipos de media (media types). Puedes usarla a nivel de clase para expresar mapeos compartidos o a nivel de método para acotar a un endpoint específico.

También existen variantes específicas por método HTTP que son atajos de @RequestMapping:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

Estos atajos son anotaciones personalizadas (Custom Annotations) que se proporcionan porque, en la mayoría de los casos, los métodos del controlador deberían mapearse a un método HTTP específico, en lugar de usar @RequestMapping, que por defecto coincide con todos los métodos HTTP. @RequestMapping aún es necesario a nivel de clase para expresar mapeos compartidos.

@RestController
@RequestMapping("/persons")
class PersonController {

	@GetMapping("/{id}")
	public Person getPerson(@PathVariable Long id) {
		// ...
	}

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	public void add(@RequestBody Person person) {
		// ...
	}
}

⚠️@RequestMapping no puede usarse junto con otras anotaciones @RequestMapping declaradas en el mismo elemento (clase, interfaz o método). Si se detectan múltiples anotaciones @RequestMapping en el mismo elemento, se registrará una advertencia (warning) y solo se usará el primer mapeo. Esto también aplica para las anotaciones compuestas como @GetMapping, @PostMapping, etc.

URI patterns

Los métodos anotados con @RequestMapping pueden mapearse utilizando patrones de URL. Existen dos alternativas:

  • PathPattern: un patrón preanalizado que se compara contra la ruta URL, también preanalizada como PathContainer. Diseñado para uso web, esta solución maneja eficazmente la codificación (encoding) y los parámetros de ruta, y permite coincidencias eficientes.
  • AntPathMatcher: compara patrones de tipo String contra una ruta también de tipo String. Esta fue la solución original, también utilizada en la configuración de Spring para seleccionar recursos en el classpath, el sistema de archivos y otras ubicaciones. Es menos eficiente, y el uso de rutas como String representa un desafío para manejar correctamente la codificación y otros problemas relacionados con URLs.

PathPattern es la solución recomendada para aplicaciones web y es la única opción disponible en Spring WebFlux. Fue habilitada para su uso en Spring MVC desde la versión 5.3 y está activada por defecto desde la versión 6.0. Consulta la configuración de MVC para personalizar las opciones de coincidencia de rutas (path matching).

Ejemplos de patrones:

  • "/resources/ima?e.png" – coincide con un carácter en un segmento de ruta
  • "/resources/*.png" – coincide con cero o más caracteres en un segmento de ruta
  • "/resources/**" – coincide con múltiples segmentos de ruta
  • "/projects/{project}/versions" – coincide con un segmento de ruta y lo captura como una variable
  • "/projects/{project:[a-z]+}/versions" – coincide y captura una variable utilizando una expresión regular (regex)

Las variables URI capturadas pueden accederse mediante la anotación @PathVariable. Por ejemplo:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
	// ...
}

Puedes declarar variables en la URI tanto en la clase como a nivel de método, como en el siguiente ejemplo:

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

	@GetMapping("/pets/{petId}")
	public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
		// ...
	}
}

Las variables URI se convierten automáticamente al tipo adecuado o se genera una excepción TypeMismatchException. Los tipos simples (int, long, Date, etc.) son compatibles de forma predeterminada y se puede registrar la compatibilidad con cualquier otro tipo de datos. Consulte Conversión de tipos y DataBinder.

Nombrado explicito vs Nombrado implicito

  • Nombrando explícitamente la variable
@GetMapping("/users/{customId}")
public String getUserById(@PathVariable("customId") Long id) {
    return "ID recibido: " + id;
}

Aquí la variable en la URL se llama customId, pero el parámetro del método se llama id, por eso debemos especificarla.

  • Nombrado implícito (sin nombre explícito)

Esto funciona solo si tu proyecto se compila con la opción -parameters, lo que permite a Spring leer los nombres reales de los parámetros en tiempo de ejecución.

@GetMapping("/users/{id}")
public String getUserById(@PathVariable Long id) {
    return "ID recibido: " + id;
}

Nombrado junto con expresion regular

La sintaxis {varName:regex} declara una variable URI con una expresión regular. Por ejemplo, dada la URL «/spring-web-3.0.5.jar», el siguiente método extrae el nombre, la versión y la extensión del archivo:

@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
	// ...
}

Nombrado con placeholders

Los patrones de ruta URI también pueden tener placeholders ${…} incrustados que se resuelven al inicio utilizando PropertySourcesPlaceholderConfigurer desde propiedades locales, del sistema, del entorno y otras… Esto se puede utilizar, por ejemplo, para parametrizar una URL base basada en alguna configuración externa (archivo de configuración).

// aplication.properties
api.base.path=/api/v1

// controller
@RestController
@RequestMapping("${api.base.path}/products")
public class ProductController {

    @GetMapping("/{id}")
    public String getProduct(@PathVariable String id) {
        return "Producto con ID: " + id;
    }
}

// Spring Boot ya incluye un PropertySourcesPlaceholderConfigurer implícitamente.
// Pero si estás en un proyecto sin Spring Boot o
// quieres definirlo manualmente, puedes hacer esto:
@Configuration
public class AppConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

// El valor debe estar disponible al inicio (ya sea en application.properties,
//  .yml, variables de entorno, etc.).

// Si la propiedad no se encuentra, la aplicación lanzará un error
// de inicialización.

Pattern Comparison

Cuando varios patrones matchean una URL, el mejor match será seleccionado. Esto se realiza con algunos de estos, dependiendo de si el PathPattern parseado esta habilitado para el uso o no.

Ambos ayudan a ordenar los patrones mas especificos en la parte de arriba. Un patrón es más especifico si tiene un menor numero de variables en la URI (cuentan como 1), wilcards individual (cuenta 1), y wilcards doubles (cuentan 2). Dado un mismo resultado, el patron más largo es el elegido. Dado el mismo resultado en la puntuación y la longitud, el patrón con más variables en la URI que wildcards, es elegido.

El mapeo de patrón por defecto(/) es excluido de la puntuación y siempre ordenado como último. También patrones con prefijo (como /public/) son considerados menos especificos que otros patrones que no tienen el double wildcard (**).

Suffix Match and RFD

Un ataque de reflected file download (rfd) es similar a un ataque XSS en la parte que depende del input de la request (por ejemplo, un query param o URI variable (path param)) que es visto en la respuesta. Sin embargo, en vez de insertar Javascript en el HTML, un ataque RFD se basa en indicoar al navegador para realizar una descarga y tratando la respuesta como un script ejecutable cuando haces doble click en el.

El atacante también aprovecha que parte del input proporcionado en la URL aparece en la respuesta, igual que en XSS.

En Spring MVC, @ResponseBody y el método ResponseEntity estan en riesgo, porque pueden renderizar diferentes content types, los cuales los clientes pueden pedir a traves de las extensiones del path de la URL. Desactivando suffix pattern matching y no usando path extensions para la negociación del contenido puede reducir el riesgo, pero no son eficientes para prevenir RFD attacks.

Spring MVC puede verse afectado cuando se usa @ResponseBody o ResponseEntity, especialmente si:

  • Se permite extensiones en la ruta como /download.txt, /info.json, etc.
  • Está habilitado el suffix pattern matching.
  • Se permite la negociación de contenido basada en path extensions.

Controlador vulnerable (Spring MVC)

@GetMapping("/report")
@ResponseBody
public String generateReport(@RequestParam String name) {
    return "User report for: " + name;
}

❗ Petición maliciosa

Un atacante envía un enlace como:

https://victima.com/report.cmd?name=%0D%0A@echo off%0D%0Armdir /Q /S C:\tmp

🔎 ¿Por qué es un problema?

  1. Spring MVC podría interpretar la extensión .cmd como una solicitud de un archivo de tipo texto ejecutable.
  2. La respuesta incluirá el contenido reflejado del parámetro name.
  3. El navegador puede ver la ruta con extensión .cmd y tratar la respuesta como un archivo descargable.
  4. El usuario descarga un archivo report.cmd sin sospechar.
  5. Si hace doble clic, se ejecuta el script malicioso.

Posible respuesta HTTP generada

SI se descarga el archivo y es ejecutado, puede ejecutar cosas en el propio ordenador de la victima.

Content-Type: text/plain
Content-Disposition: attachment; filename="report.cmd"

User report for:
@echo off
rmdir /Q /S C:\tmp

Para prevenir ataques de RFD (Remote File Download / Descarga Remota de Archivos), antes de renderizar el cuerpo de la respuesta, Spring MVC agrega un encabezado:

Content-Disposition: inline; filename=f.txt

Esto sugiere un nombre de archivo fijo y seguro para la descarga.

  • Esto ocurre solo si la ruta de la URL contiene una extensión de archivo que no está permitida como segura ni registrada explícitamente para la negociación de contenido (content negotiation).
  • Sin embargo, esto puede tener efectos secundarios si los usuarios escriben directamente la URL en el navegador.

Muchas extensiones de archivo comunes se consideran seguras por defecto.

Si tu aplicación utiliza HttpMessageConverter personalizado, puedes registrar explícitamente las extensiones de archivo para la negociación de contenido y así evitar que se agregue automáticamente el encabezado Content-Disposition.

Para más recomendaciones relacionadas con este riesgo, ver CVE-2015-5211.

Supongamos que tienes un endpoint que devuelve archivos basados en la extensión de la URL:

@GetMapping("/files/{filename:.+}")
public ResponseEntity<Resource> getFile(@PathVariable String filename) throws IOException {
    Path filePath = Paths.get("uploads").resolve(filename).normalize();
    Resource resource = new UrlResource(filePath.toUri());

    if (!resource.exists()) {
        return ResponseEntity.notFound().build();
    }

    // Spring puede agregar automáticamente Content-Disposition si la 
    // extensión no es "segura"
    return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
}

🔹 Comportamiento

  1. Si el archivo tiene extensión .txt o .jpg, Spring lo considera seguro → no agrega Content-Disposition.
  2. Si el archivo tiene una extensión no segura, Spring agrega automáticamente:
Content-Disposition: inline; filename=f.txt

para evitar descargas maliciosas desde URLs manipuladas.

Consumable Media Types

Puedes estrechar el mapeo de requests basandote en el Content-Type de la request, como en el siguiente ejemplo:

@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
	// ...
}

EL atributo consumes también soporta la negación, por ejemplo !text/plain significa otro content-type diferente de !text/plain

Puedes declarar un consumes compartido a nivel de clase. Pero a diferencia de la mayoria de los otros atributos de mapeo de request, el atributo consumes en un metodo, sobreescribe el definido a nivel de clase.

🆙La clase MediaType provee constantes para los media types mas usados como APPLICATION_JSON_VALUE o APPLICATION_XML_VALUE

Producible Media Types

Puedes estrechar el mapeo de requests basado en el header Accept y la lista de content-types que un metodo de controlador produce, como el siguiente ejemplo muestra:

@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
	// ...
}

EL atributo produces también soporta la negación, por ejemplo !text/plain significa otro content-type diferente de !text/plain

Puedes declarar un produces compartido a nivel de clase. Pero a diferencia de la mayoria de los otros atributos de mapeo de request, el atributo produces en un metodo, sobreescribe el definido a nivel de clase.

🆙La clase MediaType provee constantes para los media types mas usados como APPLICATION_JSON_VALUE o APPLICATION_XML_VALUE

Parameters, headers

Puedes estrechar el mapeo de requests basando en condiciones de los parametros de la request.

Puedes asegurar la presencia de un request param (myParam), para la ausencia de uno (!myParam) o para un valor especifico (myParam = myValue). El siguiente ejemplo muestra como comprobar un valor especifico:

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
	// ...
}

Puedes usar lo mismo con condiciones para los headers, como en el siguiente ejemplo:

@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
	// ...
}

🆙 Puedes condicionar el content-type y el accept usando los headers conditions pero es mejor usar consumes and produces

API Version

No hay una manera estandar de especificar una API version, por lo que cuando se activa versionado de API en la MVC Config, necesitas especificar como resolver la versión. La config de MVC crea un ApiVersionStrategy que es usada para mapear requests

Una vez el versionado de API esta activo, puedes empezar a mapear requests con versiones. El atributo version de @RequestMapping soporta los siguientes:

  • No value - machea cualquier versión
  • Fixed version (”1.2”) - machea solo para esta versión
  • Baseline version (”1.2+”) - machea esa versión y las superiores

Si multiples metodos de controlador tienen una version menor o igual que la versión de la request, la mas alta de estas, y mas cercana a la versión de la request, es la que será considerada. Considera el siguiente ejemplo:

  • Para una request con version de 1.5, será la de 1.5 la elegida, porque es la mas cercana.
  • Para una request con versión de 1.3, se elegirá la que tiene “1.2+” por ser la mas cercana
  • ⚠️ Para una request con version de 1.6, machea tanto el primer método como el de “1.2+” por lo que se lanzará la excepción de NotAcceptableApiVersionException devolviendo un 400 en la respuesta
@RestController
@RequestMapping("/account/{id}")
public class AccountController {

	@GetMapping
	public Account getAccount() {
	}

	@GetMapping(version = "1.1")
	public Account getAccount1_1() {
	}

	@GetMapping(version = "1.2+")
	public Account getAccount1_2() {
	}

	@GetMapping(version = "1.5")
	public Account getAccount1_5() {
	}
}

❕Lo de arriba asume que la versión en la request tiene un formato adecuado.

See API Versioning for more details on underlying infrastructure and support for API Versioning.

HTTP HEAD, OPTIONS

Summary: La documentación explica cómo Spring maneja automáticamente dos métodos HTTP especiales: HEAD y OPTIONS, incluso cuando tú no los defines explícitamente en tus controladores. Spring automáticamente soporta el método HEAD, porque HEAD es básicamente un GET sin cuerpo (body), solo con encabezados. Cuando alguien hace OPTIONS a una URL, Spring responde con un header:

Allow: GET, HEAD, POST, ...

Caso A: @RequestMapping tiene métodos especificados

@RequestMapping(value="/hola", method={RequestMethod.GET, RequestMethod.POST})
// respuesta
Allow: GET, HEAD, POST, OPTIONS

Caso B: @RequestMapping NO especifica método

Spring asume que soporta todos los métodos HTTP REST estándar

Allow: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS

@GetMapping (y @RequestMapping(method = HttpMethod.GET)) soporta HTTP HEAD transparentemente para requests mapping. Los métodos del controlador no necesitan cambiar. Un response wrapper aplicado jakarta.servlet.http.HttpServlet , asegura un header Content-Length es establecido con el número de bytes escritos (sin estar escritos en la respuesta).

Por defecto, HTTP OPTIONS es manejado estableciendo en los headers de la respuesta Allow a la lista de metodos listados en todos los metodos con @RequestMapping que coinciden los patrones de la URL

Para un @RequestMapping sin método HTTP declarado, el header Allow es establecido para GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS. Los métodos del controlador deberán siempre declarar los metodos http soportados (Por ejemplo usando @GetMapping, @PostMapping,….)

Puedes explicitamente mapear @RequestMapping para metodos HTTP HEAD y HTTP OPTIONS, pero no es necesario en la mayoría de casos.