26. Servicio con token

Versión para imprimir.

A. Introducción

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

  1. Prueba el ejemplo en https://srvtoken.rf.gd/.

  2. Descarga el archivo /src/srvtoken.zip y descompáctalo.

  3. Crea tu proyecto en GitHub:

    1. Crea una cuenta de email, por ejemplo, pepito@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.

      • Mantén la selección Public para que otros usuarios puedan ver tu proyecto.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

  4. Importa el proyecto en GitHub:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.

  5. Edita los archivos que desees.

  6. Haz clic derecho en index.html, selecciona PHP Server: serve project y se abre el navegador para que puedas probar localmente el ejemplo.

  7. Para depurar paso a paso haz lo siguiente:

    1. En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.

    2. Recarga la página, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    3. Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).

    4. Selecciona el archivo donde vas a empezar a depurar.

    5. Haz clic en el número de la línea donde vas a empezar a depurar.

    6. En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.

    7. Haz clic en Run and Debug .

    8. Si no está configurada la depuración, haz clic en create a launch json file.

    9. Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .

    10. Aparece un cuadro con los controles de depuración

    11. Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.

    12. Regresa al navegador, recarga la página y empieza a usarla.

    13. Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.

  8. Sube el proyecto al hosting que elijas sin incluir el archivo .htaccess. En algunos casos puedes usar filezilla (https://filezilla-project.org/)

  9. En algunos host como InfinityFree, tienes que configurar el certificado SSL.

  10. En algunos host como InfinityFree, debes subir el archivo .htaccess cuando el certificado SSL se ha creado e instalado. Sirve para forzar el uso de https.

  11. Abre un navegador y prueba el proyecto en tu hosting.

  12. En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.

  13. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push

D. Archivos

Haz clic en los triángulos para expandir las carpetas

E. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Uso de tokens</title>
10
11</head>
12
13<body>
14
15 <h1>Uso de tokens</h1>
16
17 <p>
18 <a href="formulario.html">Servicio que procesa un formulario con token.</a>
19 </p>
20
21 </form>
22
23</body>
24
25</html>

F. formulario.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Servicio que procesa un formulario con token</title>
10
11 <script type="module" src="lib/js/consumeJson.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<!-- Al cargar, registra este formulario en la sesión y recibe un token. -->
18
19<body onload="consumeJson('srv/registra.php')
20 .then(token => forma.token.value = token.body)
21 .catch(muestraError)">
22
23 <form id="forma" onsubmit="submitForm('srv/procesa.php', event)
24 .then(resultado => {
25 alert(resultado.body)
26 location.href = 'index.html'
27 })
28 .catch(muestraError)">
29
30 <h1>Servicio que procesa un formulario con token</h1>
31
32 <p><a href="index.html">Cancela</a></p>
33
34 <!-- Al enviar la forma, se envía el token recibido. -->
35 <input type="hidden" name="token">
36
37 <p>
38 <label>
39 Saludo:
40 <input name="saludo">
41 </label>
42 </p>
43
44 <p>
45 <label>
46 Nombre:
47 <input name="nombre">
48 </label>
49 </p>
50
51 <p>
52 <button type="reset">Restaurar</button>
53 <button type="submit">Procesa</button>
54 </p>
55
56 </form>
57
58</body>
59
60</html>

G. Carpeta « srv »

Versión para imprimir.

A. srv / procesa.php

1<?php
2
3require_once __DIR__ . "/../lib/php/BAD_REQUEST.php";
4require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
5require_once __DIR__ . "/../lib/php/recuperaTexto.php";
6require_once __DIR__ . "/../lib/php/validaToken.php";
7require_once __DIR__ . "/../lib/php/recuperaTexto.php";
8require_once __DIR__ . "/../lib/php/ProblemDetails.php";
9require_once __DIR__ . "/../lib/php/devuelveJson.php";
10
11ejecutaServicio(function () {
12
13 session_start();
14
15 $token = recuperaTexto("token");
16 if ($token === false) {
17 throw new ProblemDetails(
18 status: BAD_REQUEST,
19 title: "Falta el token.",
20 type: "/error/faltatoken.html",
21 );
22 }
23
24 validaToken("formulario", $token);
25
26 // Si el token se halló, precesa normalmente la forma.
27
28 $saludo = recuperaTexto("saludo");
29 $nombre = recuperaTexto("nombre");
30
31 if ($saludo === false)
32 throw new ProblemDetails(
33 status: BAD_REQUEST,
34 title: "Falta el saludo.",
35 type: "/error/faltasaludo.html",
36 detail: "La solicitud no tiene el valor de saludo.",
37 );
38
39 if ($saludo === "")
40 throw new ProblemDetails(
41 status: BAD_REQUEST,
42 title: "El saludo está en blanco.",
43 type: "/error/saludoenblanco.html",
44 detail: "Pon texto en el campo saludo.",
45 );
46
47 if ($nombre === false)
48 throw new ProblemDetails(
49 status: BAD_REQUEST,
50 title: "Falta el nombre.",
51 type: "/error/faltanombre.html",
52 detail: "La solicitud no tiene el valor de nombre.",
53 );
54
55 if ($nombre === "")
56 throw new ProblemDetails(
57 status: BAD_REQUEST,
58 title: "El nombre está en blanco.",
59 type: "/error/nombreenblanco.html",
60 detail: "Pon texto en el campo nombre.",
61 );
62
63 $resultado = "{$saludo} {$nombre}.";
64
65 devuelveJson($resultado);
66});
67

B. srv / registra.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/creaToken.php";
5require_once __DIR__ . "/../lib/php/devuelveJson.php";
6
7ejecutaServicio(function () {
8 session_start();
9 // Crea un token para la página "formulario" que expira en 5 minutos.
10 devuelveJson(creaToken("formulario", 5));
11});
12

H. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

1. lib / js / consumeJson.js

1import { exportaAHtml } from "./exportaAHtml.js"
2import { ProblemDetails } from "./ProblemDetails.js"
3
4/**
5 * Espera a que la promesa de un fetch termine. Si
6 * hay error, lanza una excepción. Si no hay error,
7 * interpreta la respuesta del servidor como JSON y
8 * la convierte en una literal de objeto.
9 *
10 * @param { string | Promise<Response> } servicio
11 */
12export async function consumeJson(servicio) {
13
14 if (typeof servicio === "string") {
15 servicio = fetch(servicio, {
16 headers: { "Accept": "application/json, application/problem+json" }
17 })
18 } else if (!(servicio instanceof Promise)) {
19 throw new Error("Servicio de tipo incorrecto.")
20 }
21
22 const respuesta = await servicio
23
24 const headers = respuesta.headers
25
26 if (respuesta.ok) {
27 // Aparentemente el servidor tuvo éxito.
28
29 if (respuesta.status === 204) {
30 // No contiene texto de respuesta.
31
32 return { headers, body: {} }
33
34 } else {
35
36 const texto = await respuesta.text()
37
38 try {
39
40 return { headers, body: JSON.parse(texto) }
41
42 } catch (error) {
43
44 // El contenido no es JSON. Probablemente sea texto de un error.
45 throw new ProblemDetails(respuesta.status, headers, texto,
46 "/error/errorinterno.html")
47
48 }
49
50 }
51
52 } else {
53 // Hay un error.
54
55 const texto = await respuesta.text()
56
57 if (texto === "") {
58
59 // No hay texto. Se usa el texto predeterminado.
60 throw new ProblemDetails(respuesta.status, headers, respuesta.statusText)
61
62 } else {
63 // Debiera se un ProblemDetails en JSON.
64
65 try {
66
67 const { title, type, detail } = JSON.parse(texto)
68
69 throw new ProblemDetails(respuesta.status, headers,
70 typeof title === "string" ? title : respuesta.statusText,
71 typeof type === "string" ? type : undefined,
72 typeof detail === "string" ? detail : undefined)
73
74 } catch (error) {
75
76 if (error instanceof ProblemDetails) {
77 // El error si era un ProblemDetails
78
79 throw error
80
81 } else {
82
83 throw new ProblemDetails(respuesta.status, headers, respuesta.statusText,
84 undefined, texto)
85
86 }
87
88 }
89
90 }
91
92 }
93
94}
95
96exportaAHtml(consumeJson)

2. lib / js / exportaAHtml.js

1/**
2 * Permite que los eventos de html usen la función.
3 * @param {function} functionInstance
4 */
5export function exportaAHtml(functionInstance) {
6 window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance
7}
8
9/**
10 * @param {function} valor
11 */
12export function nombreDeFuncionParaHtml(valor) {
13 const names = valor.name.split(/\s+/g)
14 return names[names.length - 1]
15}

3. lib / js / muestraError.js

1import { exportaAHtml } from "./exportaAHtml.js"
2import { ProblemDetails } from "./ProblemDetails.js"
3
4/**
5 * Muestra un error en la consola y en un cuadro de
6 * alerta el mensaje de una excepción.
7 * @param { ProblemDetails | Error | null } error descripción del error.
8 */
9export function muestraError(error) {
10
11 if (error === null) {
12
13 console.error("Error")
14 alert("Error")
15
16 } else if (error instanceof ProblemDetails) {
17
18 let mensaje = error.title
19 if (error.detail) {
20 mensaje += `\n\n${error.detail}`
21 }
22 mensaje += `\n\nCódigo: ${error.status}`
23 if (error.type) {
24 mensaje += ` ${error.type}`
25 }
26
27 console.error(mensaje)
28 console.error(error)
29 console.error("Headers:")
30 error.headers.forEach((valor, llave) => console.error(llave, "=", valor))
31 alert(mensaje)
32
33 } else {
34
35 console.error(error)
36 alert(error.message)
37
38 }
39
40}
41
42exportaAHtml(muestraError)

4. lib / js / ProblemDetails.js

1/**
2 * Detalle de los errores devueltos por un servicio.
3 */
4export class ProblemDetails extends Error {
5
6 /**
7 * @param {number} status
8 * @param {Headers} headers
9 * @param {string} title
10 * @param {string} [type]
11 * @param {string} [detail]
12 */
13 constructor(status, headers, title, type, detail) {
14 super(title)
15 /**
16 * @readonly
17 */
18 this.status = status
19 /**
20 * @readonly
21 */
22 this.headers = headers
23 /**
24 * @readonly
25 */
26 this.type = type
27 /**
28 * @readonly
29 */
30 this.detail = detail
31 /**
32 * @readonly
33 */
34 this.title = title
35 }
36
37}

5. lib / js / submitForm.js

1import { consumeJson } from "./consumeJson.js"
2import { exportaAHtml } from "./exportaAHtml.js"
3
4/**
5 * Envía los datos de la forma a la url usando la codificación
6 * multipart/form-data.
7 * @param {string} url
8 * @param {Event} event
9 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
10 * | "CONNECT" | "HEAD" } metodoHttp
11 */
12export function submitForm(url, event, metodoHttp = "POST") {
13
14 event.preventDefault()
15
16 const form = event.target
17
18 if (!(form instanceof HTMLFormElement))
19 throw new Error("event.target no es un elemento de tipo form.")
20
21 return consumeJson(fetch(url, {
22 method: metodoHttp,
23 headers: { "Accept": "application/json, application/problem+json" },
24 body: new FormData(form)
25 }))
26
27}
28
29exportaAHtml(submitForm)

B. Carpeta « lib / php »

1. lib / php / BAD_REQUEST.php

1<?php
2
3const BAD_REQUEST = 400;
4

2. lib / php / creaToken.php

1<?php
2
3function creaToken(string $pagina, int $duracionEnMinutos)
4{
5 $criptografiaFuerte = true;
6
7 // Crea el token
8 $token = [
9 "expiracion" => time() + 60 * $duracionEnMinutos,
10 // El token es de 80 caracteres, criptográficamente fuerte.
11 "texto" => bin2hex(openssl_random_pseudo_bytes(80, $criptografiaFuerte))
12 ];
13
14 // Verifica que ya haya tokens $pagina.
15 if (isset($_SESSION[$pagina])) {
16
17 $tokensParaPagina = $_SESSION[$pagina];
18
19 // Como ya existe el arreglo, elimina los tokens expirados para esta pagina.
20 foreach ($tokensParaPagina as $llave => $tokenParaPagina) {
21 if ($tokenParaPagina["expiracion"] > time()) {
22 unset($tokensParaPagina[$llave]);
23 }
24 }
25
26 // Se puede usar uno o varios tokens por pagina.
27 $tokensParaPagina[] = $token;
28 $_SESSION[$pagina] = $tokensParaPagina;
29 } else {
30
31 // Se puede usar uno o varios tokens por pagina
32 $_SESSION[$pagina] = [$token];
33 }
34
35 return $token["texto"];
36}
37

3. lib / php / devuelveErrorInterno.php

1<?php
2
3require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4require_once __DIR__ . "/devuelveProblemDetails.php";
5require_once __DIR__ . "/devuelveProblemDetails.php";
6
7function devuelveErrorInterno(Throwable $error)
8{
9 devuelveProblemDetails(new ProblemDetails(
10 status: INTERNAL_SERVER_ERROR,
11 title: $error->getMessage(),
12 type: "/error/errorinterno.html"
13 ));
14}
15

4. lib / php / devuelveJson.php

1<?php
2
3require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5function devuelveJson($resultado)
6{
7
8 $json = json_encode($resultado);
9
10 if ($json === false) {
11
12 devuelveResultadoNoJson();
13 } else {
14
15 http_response_code(200);
16 header("Content-Type: application/json");
17 echo $json;
18 }
19}
20

5. lib / php / devuelveProblemDetails.php

1<?php
2
3require_once __DIR__ . "/devuelveResultadoNoJson.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6function devuelveProblemDetails(ProblemDetails $details)
7{
8
9 $body = ["title" => $details->title];
10 if ($details->type !== null) {
11 $body["type"] = $details->type;
12 }
13 if ($details->detail !== null) {
14 $body["detail"] = $details->detail;
15 }
16
17 $json = json_encode($body);
18
19 if ($json === false) {
20
21 devuelveResultadoNoJson();
22 } else {
23
24 http_response_code($details->status);
25 header("Content-Type: application/problem+json");
26 echo $json;
27 }
28}
29

6. lib / php / devuelveResultadoNoJson.php

1<?php
2
3require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5function devuelveResultadoNoJson()
6{
7
8 http_response_code(INTERNAL_SERVER_ERROR);
9 header("Content-Type: application/problem+json");
10 echo '{' .
11 '"title": "El resultado no puede representarse como JSON."' .
12 '"type": "/error/resultadonojson.html"' .
13 '}';
14}
15

7. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4require_once __DIR__ . "/devuelveProblemDetails.php";
5require_once __DIR__ . "/devuelveErrorInterno.php";
6
7function ejecutaServicio(callable $codigo)
8{
9 try {
10 $codigo();
11 } catch (ProblemDetails $details) {
12 devuelveProblemDetails($details);
13 } catch (Throwable $error) {
14 devuelveErrorInterno($error);
15 }
16}
17

8. lib / php / INTERNAL_SERVER_ERROR.php

1<?php
2
3const INTERNAL_SERVER_ERROR = 500;

9. lib / php / ProblemDetails.php

1<?php
2
3/** Detalle de los errores devueltos por un servicio. */
4class ProblemDetails extends Exception
5{
6
7 public int $status;
8 public string $title;
9 public ?string $type;
10 public ?string $detail;
11
12 public function __construct(
13 int $status,
14 string $title,
15 ?string $type = null,
16 ?string $detail = null,
17 Throwable $previous = null
18 ) {
19 parent::__construct($title, $status, $previous);
20 $this->status = $status;
21 $this->type = $type;
22 $this->title = $title;
23 $this->detail = $detail;
24 }
25}
26

10. lib / php / recuperaTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 *
7 * Si el parámetro no se recibe, devuelve false.
8 */
9function recuperaTexto(string $parametro): false|string
10{
11 /* Si el parámetro está asignado en $_REQUEST,
12 * devuelve su valor; de lo contrario, devuelve false.
13 */
14 $valor = isset($_REQUEST[$parametro])
15 ? $_REQUEST[$parametro]
16 : false;
17 return $valor;
18}
19

11. lib / php / validaToken.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5const FORBIDDEN = 403;
6
7function validaToken(string $pagina, string $token)
8{
9
10 if (!isset($_SESSION[$pagina]))
11 throw new ProblemDetails(
12 status: FORBIDDEN,
13 title: "Página no registrada.",
14 type: "/error/paginanoregistrada.html",
15 );
16
17 $tokensParaPagina = $_SESSION[$pagina];
18
19 if (!is_array($tokensParaPagina))
20 throw new ProblemDetails(
21 status: FORBIDDEN,
22 title: "No hay arereglo de tokens.",
23 type: "/error/sintokens.html",
24 );
25
26 $hallado = false;
27
28 // Valida que el token se haya registrado.
29 foreach ($tokensParaPagina as $llave => $tokenParaPagina) {
30
31 if (strcmp($token, $tokenParaPagina["texto"]) === 0) {
32
33 if ($tokenParaPagina["expiracion"] < time()) {
34 unset($tokensParaPagina[$llave]);
35 $_SESSION[$pagina] = $tokensParaPagina;
36 throw new ProblemDetails(
37 status: FORBIDDEN,
38 title: "Tiempo de expiración excedido.",
39 type: "/error/paginaexpirada.html",
40 );
41 }
42
43 $hallado = true;
44 } elseif ($tokenParaPagina["expiracion"] > time()) {
45
46 // Elimina tokens expirados
47 unset($tokensParaPagina[$llave]);
48 }
49 }
50
51 $_SESSION[$pagina] = $tokensParaPagina;
52
53 if ($hallado === false)
54 throw new ProblemDetails(
55 status: FORBIDDEN,
56 title: "Página no registrada.",
57 type: "/error/paginanoregistrada.html",
58 );
59}
60

I. Carpeta « error »

Versión para imprimir.

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17 <p>La solicitud no tiene el valor de nombre.</p>
18
19</body>
20
21</html>

C. error / faltasaludo.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el saludo</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el saludo</h1>
16
17 <p>La solicitud no tiene el valor de saludo.</p>
18
19</body>
20
21</html>

D. error / faltatoken.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el token</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el token</h1>
16
17</body>
18
19</html>

E. error / nombreenblanco.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El nombre está en blanco</title>
10
11</head>
12
13<body>
14
15 <h1>El nombre está en blanco</h1>
16
17 <p>Pon texto en el campo nombre.</p>
18
19</body>
20
21</html>

F. error / paginaexpirada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Tiempo de expiración excedido</title>
10
11</head>
12
13<body>
14
15 <h1>Tiempo de expiración excedido</h1>
16
17</body>
18
19</html>

G. error / paginanoregistrada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Página no registrada</title>
10
11</head>
12
13<body>
14
15 <h1>Página no registrada</h1>
16
17</body>
18
19</html>

H. error / resultadonojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El resultado no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El resultado no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, el resultado generado, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

I. error / saludoenblanco.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El saludo está en blanco</title>
10
11</head>
12
13<body>
14
15 <h1>El saludo está en blanco</h1>
16
17 <p>Pon texto en el campo saludo.</p>
18
19</body>
20
21</html>

J. error / sintokens.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>No hay arereglo de tokens</title>
10
11</head>
12
13<body>
14
15 <h1>No hay arereglo de tokens</h1>
16
17</body>
18
19</html>

J. .htaccess

1RewriteEngine On
2RewriteCond %{HTTP:X-Forwarded-Proto} !https
3RewriteCond %{HTTPS} off
4RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"}
5RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

K. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "Node16",
7 "moduleResolution": "Node16",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

L. Resumen