Generado con IA · Supuesto práctico

Desarrollo web cliente/servidor: una aplicación de reserva de aulas

Caso único con preguntas Informática Comunidad Valenciana
Descargar:
Supuesto práctico generado por la IA de OposicionesIA, con su orientación y solución modelo más abajo. Dentro de la plataforma, además, puedes resolverlo y recibir una corrección con rúbrica y nota.

Desarrollo web cliente/servidor: una aplicación de reserva de aulas

Contexto

El IES «Jordi de Sant Jordi» (València) quiere una pequeña aplicación web para que el profesorado reserve las aulas de informática. La arquitectura es cliente/servidor: front-end en HTML5 + CSS + JavaScript que consume un servicio REST en PHP que devuelve JSON y se apoya en una base de datos MySQL. El supuesto se enmarca en el módulo «Desarrollo Web en Entorno Servidor» (2.º DAW).

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 200" font-family="ui-sans-serif, system-ui, Arial, sans-serif"> <text x="380" y="26" text-anchor="middle" font-size="12" font-weight="bold" fill="#2563eb">Arquitectura de tres capas cliente/servidor</text> <!-- Capa 1: Navegador --> <rect x="15" y="72" width="210" height="76" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-width="1.6"/> <text x="120" y="100" text-anchor="middle" font-size="13" font-weight="bold" fill="#0f172a">Navegador</text> <text x="120" y="120" text-anchor="middle" font-size="10.5" fill="#2563eb">HTML5 + CSS + JavaScript</text> <text x="120" y="139" text-anchor="middle" font-size="10" fill="#64748b">capa de presentación (cliente)</text> <!-- Flecha 1 --> <text x="250" y="102" text-anchor="middle" font-size="10" fill="#64748b">fetch · JSON</text> <line x1="225" y1="110" x2="273" y2="110" stroke="#334155" stroke-width="1.8"/> <polygon points="273,110 261,105 261,115" fill="#334155"/> <!-- Capa 2: API REST --> <rect x="275" y="72" width="210" height="76" rx="9" fill="#ecfdf5" stroke="#059669" stroke-width="1.6"/> <text x="380" y="100" text-anchor="middle" font-size="13" font-weight="bold" fill="#0f172a">API REST</text> <text x="380" y="120" text-anchor="middle" font-size="10.5" fill="#059669">PHP</text> <text x="380" y="139" text-anchor="middle" font-size="10" fill="#64748b">lógica de negocio (servidor)</text> <!-- Flecha 2 --> <text x="510" y="102" text-anchor="middle" font-size="10" fill="#64748b">SQL</text> <line x1="485" y1="110" x2="533" y2="110" stroke="#334155" stroke-width="1.8"/> <polygon points="533,110 521,105 521,115" fill="#334155"/> <!-- Capa 3: Base de datos --> <rect x="535" y="72" width="210" height="76" rx="9" fill="#fffbeb" stroke="#d97706" stroke-width="1.6"/> <text x="640" y="100" text-anchor="middle" font-size="13" font-weight="bold" fill="#0f172a">Base de datos</text> <text x="640" y="120" text-anchor="middle" font-size="10.5" fill="#d97706">MySQL</text> <text x="640" y="139" text-anchor="middle" font-size="10" fill="#64748b">capa de persistencia</text> </svg>

Figura 1. Arquitectura cliente/servidor de la aplicación.

Cuestiones

1. Estructura y maquetación. Describe la estructura HTML semántica de la página de reservas (cabecera, formulario de reserva con selección de aula/fecha/franja, y una tabla con las reservas del día) y dos buenas prácticas de accesibilidad que aplicarías. No hace falta CSS completo: indica el enfoque (p. ej. flexbox/grid y diseño responsive).

2. Cliente (JavaScript). Escribe la función JavaScript que, al enviar el formulario, realice una petición fetch (método POST, cuerpo en JSON) al endpoint /api/reservas, gestione la respuesta y los códigos de estado (201 creado, 409 conflicto de reserva, 422 datos inválidos) y muestre el mensaje adecuado al usuario sin recargar la página.

3. Servidor (PHP, REST). Implementa el endpoint POST /api/reservas en PHP: lee el JSON de entrada, valida los datos, comprueba que el aula esté libre en esa franja e inserta la reserva. Debe responder con el JSON y el código HTTP correctos en cada caso (201 / 409 / 422) y usar consultas preparadas.

4. Buscar y corregir errores. Una primera versión del endpoint, escrita con prisa, presenta problemas graves de seguridad y de protocolo. Detéctalos (al menos tres) y reescribe el fragmento correctamente, explicando el riesgo de cada uno:

<?php
$aula = $_GET['aula'];
$fecha = $_GET['fecha'];

$sql = "INSERT INTO reservas (aula, fecha) VALUES ('$aula', '$fecha')";
mysqli_query($conn, $sql);

echo "Reserva creada";
http_response_code(200);
?>

5. Mini-cuestión. Explica la diferencia entre los métodos GET y POST y por qué el alta de una reserva no debe hacerse por GET. ¿Qué relación tiene esto con la idempotencia de los métodos HTTP?

Orientación cómo resolverlo por tu cuenta

Este supuesto recorre el ciclo completo cliente/servidor de una API REST sencilla. Enfócalo demostrando que dominas el flujo HTTP de extremo a extremo y, sobre todo, la seguridad.

Cuestión 1 (HTML/maquetación). Recuerda la diferencia entre etiquetas semánticas (header, main, section, nav, footer, table) y los div genéricos. Piensa en el formulario como tres controles: aula (select), fecha (input type=date) y franja (select o radio). Para accesibilidad, ataca WCAG/WAI: asociación labelfor/id, atributos aria, contraste, navegación por teclado, scope en cabeceras de tabla. Para CSS, basta nombrar el enfoque (flexbox para la barra, grid para el formulario, media queries para responsive, mobile-first). Error típico: maquetar todo con div y olvidar los label.

Cuestión 2 (JavaScript/fetch). El camino: preventDefault, recoger datos, construir el objeto, fetch con method, headers Content-Type: application/json y body: JSON.stringify. Lo clave es ramificar según response.status (201/409/422) y usar async/await con try/catch. Error típico: olvidar JSON.stringify o no distinguir códigos.

Cuestión 3 (PHP/REST). Orden: leer php://input, json_decode, validar (422 si falla), comprobar solapamiento con un SELECT preparado (409 si ocupada), INSERT preparado, responder 201. Siempre header Content-Type: application/json, http_response_code() y sentencias preparadas con bind. Error típico: validar después de insertar.

Cuestión 4 (corregir errores). Identifica al menos tres fallos: inyección SQL (concatenación), método incorrecto ($_GET en vez de php://input/POST) y código HTTP erróneo (200 en lugar de 201). Añade falta de validación, ausencia de cabecera JSON y de control de errores. Explica el riesgo de cada uno.

Cuestión 5 (GET vs POST). Relaciona GET (seguro, idempotente, cacheable, datos en URL) frente a POST (no idempotente, no seguro, cuerpo). Un alta crea estado: por GET sería cacheable, prefetchable y repetible. Define idempotencia con precisión.

Solución modelo respuesta completa — intenta resolverlo antes de mirar

Cuestión 1 — Estructura, maquetación y accesibilidad

Se emplea HTML5 semántico: el navegador, los lectores de pantalla y los buscadores interpretan el rol de cada bloque sin depender de class. La página se organiza en header (identidad y navegación), main (contenido principal con el formulario y la tabla) y footer.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Reserva de aulas de informática · IES Jordi de Sant Jordi</title>
  <link rel="stylesheet" href="/css/estilos.css">
</head>
<body>
  <header>
    <h1>Reserva de aulas de informática</h1>
    <nav aria-label="Navegación principal">
      <a href="/">Inicio</a> · <a href="/mis-reservas">Mis reservas</a>
    </nav>
  </header>

  <main>
    <!-- Formulario de reserva -->
    <section aria-labelledby="titulo-form">
      <h2 id="titulo-form">Nueva reserva</h2>
      <form id="form-reserva" novalidate>
        <div class="campo">
          <label for="aula">Aula</label>
          <select id="aula" name="aula" required>
            <option value="">— Selecciona —</option>
            <option value="INF-1">Aula Informática 1</option>
            <option value="INF-2">Aula Informática 2</option>
          </select>
        </div>

        <div class="campo">
          <label for="fecha">Fecha</label>
          <input type="date" id="fecha" name="fecha" required>
        </div>

        <fieldset class="campo">
          <legend>Franja horaria</legend>
          <label for="franja">Franja</label>
          <select id="franja" name="franja" required>
            <option value="">— Selecciona —</option>
            <option value="1">1.ª (08:00–09:00)</option>
            <option value="2">2.ª (09:00–10:00)</option>
            <option value="3">3.ª (10:00–11:00)</option>
          </select>
        </fieldset>

        <button type="submit">Reservar</button>
        <p id="mensaje" role="status" aria-live="polite"></p>
      </form>
    </section>

    <!-- Reservas del día -->
    <section aria-labelledby="titulo-tabla">
      <h2 id="titulo-tabla">Reservas de hoy</h2>
      <table>
        <caption>Aulas reservadas en el día</caption>
        <thead>
          <tr>
            <th scope="col">Aula</th>
            <th scope="col">Franja</th>
            <th scope="col">Profesor/a</th>
          </tr>
        </thead>
        <tbody id="tbody-reservas">
          <!-- filas inyectadas por JS -->
        </tbody>
      </table>
    </section>
  </main>

  <footer>
    <p>IES «Jordi de Sant Jordi» · València</p>
  </footer>

  <script src="/js/reservas.js"></script>
</body>
</html>

Enfoque CSS (sin detallarlo entero): diseño mobile-first y responsive con media queries. La barra de navegación y los botones se alinean con flexbox; el formulario se distribuye en columnas con CSS Grid (display:grid; grid-template-columns: repeat(auto-fit, minmax(200px,1fr))), colapsando a una sola columna en móvil. La tabla se hace responsive con overflow-x:auto en su contenedor.

Dos buenas prácticas de accesibilidad (WCAG 2.1 / WAI-ARIA):

  1. Etiquetado explícito de controles: todo input/select lleva su <label for="…"> asociado al id del control, y los grupos de opciones se envuelven en fieldset/legend. Esto permite que un lector de pantalla anuncie el propósito de cada campo y aumenta el área de clic.
  2. Estructura percibible y mensajes anunciados: uso de cabeceras de tabla con scope="col" y <caption>, atributo lang="es", y una región role="status" con aria-live="polite" para que el resultado de la reserva se anuncie sin recargar. Se garantiza además navegación por teclado (orden lógico del DOM, :focus visible) y contraste suficiente (ratio ≥ 4.5:1, WCAG 1.4.3).

Cuestión 2 — Cliente (JavaScript con fetch)

Se intercepta el envío del formulario, se evita la recarga (preventDefault), se construye el cuerpo en JSON y se ramifica según el código de estado de la respuesta. Se usa async/await con try/catch para errores de red.

const form = document.getElementById('form-reserva');
const mensaje = document.getElementById('mensaje');

form.addEventListener('submit', async (e) => {
  e.preventDefault();                       // no recargar la página
  mensaje.textContent = 'Enviando…';
  mensaje.className = '';

  // 1) Recoger datos del formulario
  const datos = {
    aula:   document.getElementById('aula').value,
    fecha:  document.getElementById('fecha').value,
    franja: document.getElementById('franja').value,
  };

  try {
    // 2) Petición POST con cuerpo JSON
    const resp = await fetch('/api/reservas', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify(datos),
    });

    // 3) Gestión de códigos de estado
    if (resp.status === 201) {
      const reserva = await resp.json();
      mensaje.textContent = `Reserva creada correctamente (aula ${reserva.aula}, franja ${reserva.franja}).`;
      mensaje.className = 'ok';
      form.reset();
      // Opcional: refrescar la tabla del día
    } else if (resp.status === 409) {
      mensaje.textContent = 'Esa aula ya está reservada en la franja elegida. Prueba otra franja.';
      mensaje.className = 'error';
    } else if (resp.status === 422) {
      const err = await resp.json();
      mensaje.textContent = 'Datos inválidos: ' + (err.errores ? err.errores.join(', ') : 'revisa el formulario.');
      mensaje.className = 'error';
    } else {
      mensaje.textContent = `Error inesperado del servidor (HTTP ${resp.status}).`;
      mensaje.className = 'error';
    }
  } catch (red) {
    // 4) Error de red / conexión
    mensaje.textContent = 'No se pudo conectar con el servidor. Inténtalo de nuevo.';
    mensaje.className = 'error';
  }
});

Justificación: el Content-Type: application/json informa al servidor del formato del cuerpo; JSON.stringify serializa el objeto. La distinción por status (en vez de comprobar solo resp.ok) permite mensajes específicos: 201 (éxito), 409 (conflicto de negocio), 422 (validación). El role="status"/aria-live ya definido hace que esos mensajes sean también accesibles.

Cuestión 3 — Servidor (PHP, REST)

Endpoint POST /api/reservas. Pasos: leer el cuerpo crudo, decodificarlo, validar (→ 422), comprobar disponibilidad con consulta preparada (→ 409) e insertar con consulta preparada (→ 201). Siempre se fija la cabecera JSON y el código HTTP correcto.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 452" font-family="ui-sans-serif, system-ui, Arial, sans-serif"> <text x="380" y="24" text-anchor="middle" font-size="12" font-weight="bold" fill="#2563eb">POST /api/reservas — flujo del servidor y códigos de estado</text> <!-- Entrada --> <rect x="240" y="40" width="280" height="46" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-width="1.6"/> <text x="380" y="60" text-anchor="middle" font-size="12" font-weight="bold" fill="#0f172a">Recibe cuerpo JSON</text> <text x="380" y="77" text-anchor="middle" font-size="10" fill="#2563eb">POST /api/reservas</text> <line x1="380" y1="86" x2="380" y2="106" stroke="#334155" stroke-width="1.8"/> <polygon points="380,106 375,94 385,94" fill="#334155"/> <!-- Decisión 1: válido --> <polygon points="380,106 492,152 380,198 268,152" fill="#f8fafc" stroke="#334155" stroke-width="1.5"/> <text x="380" y="156" text-anchor="middle" font-size="12" font-weight="bold" fill="#0f172a">¿JSON válido?</text> <text x="392" y="214" font-size="10" fill="#64748b">sí</text> <text x="520" y="146" font-size="10" fill="#64748b">no</text> <!-- rama no -> 422 --> <line x1="492" y1="152" x2="558" y2="152" stroke="#334155" stroke-width="1.6"/> <polygon points="558,152 546,147 546,157" fill="#334155"/> <rect x="560" y="128" width="192" height="48" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/> <text x="576" y="159" font-size="18" font-weight="bold" fill="#dc2626">422</text> <text x="614" y="148" font-size="10" font-weight="bold" fill="#0f172a">Unprocessable</text> <text x="614" y="164" font-size="10" fill="#dc2626">datos inválidos</text> <!-- baja a decisión 2 --> <line x1="380" y1="198" x2="380" y2="220" stroke="#334155" stroke-width="1.8"/> <polygon points="380,220 375,208 385,208" fill="#334155"/> <!-- Decisión 2: aula libre --> <polygon points="380,220 492,266 380,312 268,266" fill="#f8fafc" stroke="#334155" stroke-width="1.5"/> <text x="380" y="270" text-anchor="middle" font-size="12" font-weight="bold" fill="#0f172a">¿Aula libre?</text> <text x="392" y="328" font-size="10" fill="#64748b">sí</text> <text x="520" y="260" font-size="10" fill="#64748b">no</text> <!-- rama no -> 409 --> <line x1="492" y1="266" x2="558" y2="266" stroke="#334155" stroke-width="1.6"/> <polygon points="558,266 546,261 546,271" fill="#334155"/> <rect x="560" y="242" width="192" height="48" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/> <text x="576" y="273" font-size="18" font-weight="bold" fill="#d97706">409</text> <text x="614" y="262" font-size="10" font-weight="bold" fill="#0f172a">Conflict</text> <text x="614" y="278" font-size="10" fill="#d97706">aula ocupada</text> <!-- baja a INSERT --> <line x1="380" y1="312" x2="380" y2="332" stroke="#334155" stroke-width="1.8"/> <polygon points="380,332 375,320 385,320" fill="#334155"/> <rect x="290" y="332" width="180" height="42" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.6"/> <text x="380" y="358" text-anchor="middle" font-size="12" font-weight="bold" fill="#0f172a">INSERT reserva</text> <!-- baja a 201 --> <line x1="380" y1="374" x2="380" y2="394" stroke="#334155" stroke-width="1.8"/> <polygon points="380,394 375,382 385,382" fill="#334155"/> <rect x="280" y="394" width="200" height="48" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="1.6"/> <text x="304" y="425" font-size="18" font-weight="bold" fill="#059669">201</text> <text x="344" y="414" font-size="10" font-weight="bold" fill="#0f172a">Created</text> <text x="344" y="430" font-size="10" fill="#059669">reserva registrada</text> </svg>

Figura 1. Flujo de la petición y códigos de estado.

<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');

// 0) Solo se admite POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);                       // Method Not Allowed
    header('Allow: POST');
    echo json_encode(['error' => 'Método no permitido']);
    exit;
}

// 1) Leer y decodificar el cuerpo JSON
$raw   = file_get_contents('php://input');
$datos = json_decode($raw, true);

// 2) Validación → 422 Unprocessable Entity
$errores = [];
if (!is_array($datos)) {
    $errores[] = 'Cuerpo JSON inválido';
} else {
    if (empty($datos['aula']))                                  $errores[] = 'aula obligatoria';
    if (empty($datos['fecha']) ||
        !preg_match('/^\d{4}-\d{2}-\d{2}$/', $datos['fecha']))  $errores[] = 'fecha inválida (YYYY-MM-DD)';
    if (empty($datos['franja']) || !ctype_digit((string)$datos['franja']))
                                                                $errores[] = 'franja inválida';
}
if ($errores) {
    http_response_code(422);
    echo json_encode(['errores' => $errores]);
    exit;
}

$aula   = $datos['aula'];
$fecha  = $datos['fecha'];
$franja = (int) $datos['franja'];

// 3) Conexión con manejo de errores
$conn = new mysqli('localhost', 'usuario', 'secreto', 'reservas_db');
$conn->set_charset('utf8mb4');
if ($conn->connect_errno) {
    http_response_code(500);
    echo json_encode(['error' => 'Error de conexión']);
    exit;
}

// 4) ¿Aula libre en esa franja? — consulta PREPARADA → 409 Conflict
$check = $conn->prepare(
    'SELECT 1 FROM reservas WHERE aula = ? AND fecha = ? AND franja = ? LIMIT 1'
);
$check->bind_param('ssi', $aula, $fecha, $franja);
$check->execute();
$check->store_result();
if ($check->num_rows > 0) {
    http_response_code(409);                       // Conflict
    echo json_encode(['error' => 'El aula ya está reservada en esa franja']);
    $check->close();
    $conn->close();
    exit;
}
$check->close();

// 5) Insertar — consulta PREPARADA → 201 Created
$ins = $conn->prepare(
    'INSERT INTO reservas (aula, fecha, franja) VALUES (?, ?, ?)'
);
$ins->bind_param('ssi', $aula, $fecha, $franja);

if ($ins->execute()) {
    http_response_code(201);                        // Created
    echo json_encode([
        'id'     => $conn->insert_id,
        'aula'   => $aula,
        'fecha'  => $fecha,
        'franja' => $franja,
    ]);
} else {
    http_response_code(500);
    echo json_encode(['error' => 'No se pudo crear la reserva']);
}

$ins->close();
$conn->close();

Justificación técnica:

  • Consultas preparadas (prepare/bind_param): los valores viajan separados de la sentencia SQL, lo que impide la inyección SQL (OWASP A03:2021). Es el punto más valorado.
  • Comprobación de disponibilidad antes de insertar evita reservas solapadas; en producción se reforzaría con un índice UNIQUE(aula, fecha, franja) que garantiza la integridad incluso ante concurrencia (condición de carrera), devolviendo el error de clave duplicada que se traduciría también en 409.
  • Códigos HTTP semánticos: 201 (recurso creado, idealmente con cabecera Location), 409 (conflicto con el estado actual), 422 (entidad bien formada pero semánticamente inválida), 405 (método no admitido), 500 (error de servidor).
  • header('Content-Type: application/json') y json_encode garantizan que el cliente reciba siempre JSON; utf8mb4 evita problemas con tildes y la ñ.

Cuestión 4 — Buscar y corregir errores

El fragmento original concentra varios fallos graves. Se identifican cinco:

# Error Riesgo
1 Inyección SQL: $aula y $fecha se concatenan directamente en la cadena SQL. Un atacante puede inyectar SQL y leer o alterar datos que jamás deberían ser accesibles (p. ej. ' OR '1'='1 o un UNION SELECT); el clásico '); DROP TABLE reservas;-- ilustra el vector destructivo, aunque borrar la tabla exigiría consultas apiladas (mysqli_multi_query). Es la vulnerabilidad más crítica (OWASP A03).
2 Método HTTP incorrecto: se leen los datos por $_GET, es decir, un alta que crea estado se está haciendo por GET. GET es seguro e idempotente: la URL queda en historial, logs, caché y puede ser prefetcheada o repetida, creando reservas duplicadas no deseadas. Un alta debe ir por POST.
3 Código de estado incorrecto: devuelve 200 OK para una creación; además llama a http_response_code() después del echo. Rompe el contrato REST (debería ser 201 Created) y, al fijar el código tras enviar cuerpo, puede no aplicarse (cabeceras ya enviadas).
4 Ausencia de validación de los datos de entrada. Datos malformados o vacíos llegan a la BD; debería devolver 422.
5 Sin cabecera Content-Type JSON ni control de errores de la consulta. Se responde texto plano «Reserva creada» aunque la consulta falle: el cliente no puede distinguir éxito de error.

Versión corregida del fragmento:

<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');

// (2) Solo POST, no GET
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('Allow: POST');
    echo json_encode(['error' => 'Use POST']);
    exit;
}

// Datos del cuerpo, no de la URL
$datos = json_decode(file_get_contents('php://input'), true);

// (4) Validación → 422
if (!is_array($datos) || empty($datos['aula']) ||
    empty($datos['fecha']) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $datos['fecha'])) {
    http_response_code(422);
    echo json_encode(['error' => 'Datos inválidos']);
    exit;
}

// (1) Consulta PREPARADA: sin inyección SQL
$stmt = $conn->prepare('INSERT INTO reservas (aula, fecha) VALUES (?, ?)');
$stmt->bind_param('ss', $datos['aula'], $datos['fecha']);

// (5) Control de errores
if ($stmt->execute()) {
    http_response_code(201);                 // (3) 201 Created, ANTES del cuerpo
    echo json_encode(['id' => $conn->insert_id, 'mensaje' => 'Reserva creada']);
} else {
    http_response_code(500);
    echo json_encode(['error' => 'No se pudo crear la reserva']);
}
$stmt->close();
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 244" font-family="ui-sans-serif, system-ui, Arial, sans-serif"> <text x="380" y="14" text-anchor="middle" font-size="11" font-weight="bold" fill="#2563eb">Del fragmento vulnerable a la versión segura</text> <!-- Fila a: inyección SQL --> <rect x="20" y="24" width="300" height="56" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.4"/> <text x="34" y="46" font-size="11" font-weight="bold" fill="#0f172a">SQL por concatenación de $aula</text> <text x="34" y="66" font-size="10" fill="#dc2626">inyección SQL (OWASP A03)</text> <line x1="330" y1="52" x2="430" y2="52" stroke="#334155" stroke-width="1.6"/> <polygon points="430,52 418,47 418,57" fill="#334155"/> <rect x="440" y="24" width="300" height="56" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="1.4"/> <text x="454" y="46" font-size="11" font-weight="bold" fill="#0f172a">consulta preparada (bind_param)</text> <text x="454" y="66" font-size="10" fill="#059669">valores separados de la sentencia</text> <!-- Fila b: método GET --> <rect x="20" y="94" width="300" height="56" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.4"/> <text x="34" y="116" font-size="11" font-weight="bold" fill="#0f172a">datos por $_GET (método GET)</text> <text x="34" y="136" font-size="10" fill="#dc2626">un alta no debe ir por GET</text> <line x1="330" y1="122" x2="430" y2="122" stroke="#334155" stroke-width="1.6"/> <polygon points="430,122 418,117 418,127" fill="#334155"/> <rect x="440" y="94" width="300" height="56" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="1.4"/> <text x="454" y="116" font-size="11" font-weight="bold" fill="#0f172a">cuerpo JSON por POST</text> <text x="454" y="136" font-size="10" fill="#059669">php://input + json_decode</text> <!-- Fila c: código HTTP --> <rect x="20" y="164" width="300" height="56" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.4"/> <text x="34" y="186" font-size="11" font-weight="bold" fill="#0f172a">http_response_code(200) tras echo</text> <text x="34" y="206" font-size="10" fill="#dc2626">cabeceras ya enviadas</text> <line x1="330" y1="192" x2="430" y2="192" stroke="#334155" stroke-width="1.6"/> <polygon points="430,192 418,187 418,197" fill="#334155"/> <rect x="440" y="164" width="300" height="56" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="1.4"/> <text x="454" y="186" font-size="11" font-weight="bold" fill="#0f172a">201 Created ANTES del cuerpo</text> <text x="454" y="206" font-size="10" fill="#059669">contrato REST correcto</text> </svg>

Figura 2. Los tres fallos del fragmento y su corrección.

Cuestión 5 — GET vs POST e idempotencia

GET solicita un recurso: los parámetros viajan en la URL (query string), es seguro (safe: no modifica el estado del servidor), idempotente, cacheable y puede quedar en historial, marcadores y logs. POST envía datos en el cuerpo de la petición para que el servidor cree o procese un recurso; no es seguro (modifica estado), no es idempotente y, por defecto, no es cacheable.

El alta de una reserva no debe hacerse por GET porque:

  1. Crea estado (inserta una fila): viola el principio de que GET debe ser seguro.
  2. Al ser GET cacheable y prefetcheable, el navegador o un proxy podría repetir la petición y duplicar la reserva.
  3. Los datos quedarían expuestos en la URL (historial, logs del servidor, cabecera Referer), un problema de privacidad.

Relación con la idempotencia: un método es idempotente cuando ejecutarlo N veces produce el mismo efecto sobre el estado del servidor que ejecutarlo una sola vez. Si f es el método y s el estado del servidor:

\text{estado}\big(f^{N}(s)\big) = \text{estado}\big(f(s)\big) \quad \forall\, N \ge 1

GET, PUT y DELETE son idempotentes; POST no lo es, porque cada invocación crea un recurso nuevo (N reservas: \text{estado}(f^{N}(s)) \neq \text{estado}(f(s))). Por eso el alta encaja en POST: refleja honestamente que no es repetible sin efectos. Si se quisiera una operación idempotente (p. ej. «reserva con id conocido»), se usaría PUT sobre /api/reservas/{id}, que crea o reemplaza el recurso de forma idempotente. La distinción seguro (no altera estado) frente a idempotente (efecto invariante ante repetición) es clave: GET es ambas cosas; POST no es ninguna.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 202" font-family="ui-sans-serif, system-ui, Arial, sans-serif"> <!-- Fondo tabla --> <rect x="20" y="16" width="680" height="170" rx="6" fill="#f8fafc" stroke="#334155" stroke-width="1.4"/> <!-- Cabecera --> <rect x="20" y="16" width="680" height="34" rx="6" fill="#e2e8f0" stroke="#334155" stroke-width="1.4"/> <rect x="20" y="34" width="680" height="16" fill="#e2e8f0"/> <text x="34" y="38" font-size="12" font-weight="bold" fill="#0f172a">Propiedad</text> <text x="430" y="38" text-anchor="middle" font-size="12" font-weight="bold" fill="#0f172a">GET</text> <text x="610" y="38" text-anchor="middle" font-size="12" font-weight="bold" fill="#0f172a">POST</text> <!-- Líneas de columna --> <line x1="340" y1="16" x2="340" y2="186" stroke="#334155" stroke-width="1"/> <line x1="520" y1="16" x2="520" y2="186" stroke="#334155" stroke-width="1"/> <!-- Líneas de fila --> <line x1="20" y1="84" x2="700" y2="84" stroke="#cbd5e1" stroke-width="1"/> <line x1="20" y1="118" x2="700" y2="118" stroke="#cbd5e1" stroke-width="1"/> <line x1="20" y1="152" x2="700" y2="152" stroke="#cbd5e1" stroke-width="1"/> <!-- Fila 1: Seguro --> <text x="34" y="71" font-size="11" fill="#0f172a">Seguro (no cambia estado)</text> <text x="430" y="73" text-anchor="middle" font-size="15" font-weight="bold" fill="#059669">&#10003;</text> <text x="610" y="73" text-anchor="middle" font-size="15" font-weight="bold" fill="#dc2626">&#10007;</text> <!-- Fila 2: Idempotente --> <text x="34" y="105" font-size="11" fill="#0f172a">Idempotente</text> <text x="430" y="107" text-anchor="middle" font-size="15" font-weight="bold" fill="#059669">&#10003;</text> <text x="610" y="107" text-anchor="middle" font-size="15" font-weight="bold" fill="#dc2626">&#10007;</text> <!-- Fila 3: Cacheable --> <text x="34" y="139" font-size="11" fill="#0f172a">Cacheable</text> <text x="430" y="141" text-anchor="middle" font-size="15" font-weight="bold" fill="#059669">&#10003;</text> <text x="610" y="141" text-anchor="middle" font-size="15" font-weight="bold" fill="#dc2626">&#10007;</text> <!-- Fila 4: Datos en --> <text x="34" y="173" font-size="11" fill="#0f172a">Datos en…</text> <text x="430" y="173" text-anchor="middle" font-size="10.5" fill="#0f172a">URL (query string)</text> <text x="610" y="173" text-anchor="middle" font-size="10.5" fill="#0f172a">cuerpo (body)</text> </svg>

Figura 3. GET frente a POST e idempotencia.

Practica con supuestos como este

Genera supuestos de tu especialidad y comunidad en cualquiera de los cuatro formatos, resuélvelos y recibe corrección con nota al instante.