Web

Ultima Modifica / Se Modificato Da

By
16 novembre 2023
4 minuti di lettura
Ultima Modifica / Se Modificato Da

Codice di supporto

Manteniamo tutto semplice - NodeJS, senza dipendenze. Costruiamo insieme alcuni endpoint, ognuno con header diversi, e scopriamo come si comporta il browser in base agli header ricevuti.

Vai direttamente all’endpoint /no-headers o dai un’occhiata (molto veloce) al server più semplice che ci sia.

import { createServer } from "http";
import noHeaders from "./src/index.mjs";

createServer((req, res) => {
  switch (req.url) {
	case "/no-headers":
	  return noHeaders(req, res);
  }
}).listen(8000, "127.0.0.1", () =>
  console.info("Esposto su http://127.0.0.1:8000")
);
import fs from "fs/promises";
import path from "path";

export function to(promise) {
  return promise.then((res) => [res, null]).catch((err) => [null, err]);
}

export async function getView(name) {
  const filepath = path.resolve(
	process.cwd(),
	"src",
	"views",
	name + ".html"
  );
  return await to(fs.readFile(filepath, "utf-8"));
}

export async function getViewStats(name) {
  const filepath = path.resolve(process.cwd(), "src", "views", name + ".html");
  return await to(fs.stat(filepath));
}

Aggiungi un file HTML in src/views/index.html. Il suo contenuto è irrilevante.


No Headers - Endpoint

Legge semplicemente il file e lo invia all’utente. A parte il Content-Type, non viene aggiunto alcun header relativo alla cache.

import { getView } from "./utils.mjs";

export default async (req, res) => {
  res.setHeader("Content-Type", "text/html");

  const [html, err] = await getView("index");
  if (err) {
	res.writeHead(500).end("Errore interno del server");
	return;
  }

  res.writeHead(200).end(html);
};

Avvia il server (node index.mjs), apri /no-headers, e controlla la scheda strumenti per sviluppatori > rete. Abilita preserva log e premi aggiorna alcune volte.

La scheda di rete degli strumenti per sviluppatori che mostra il documento mai memorizzato nella cache, sempre recuperato.

Apri uno qualsiasi di essi e controlla gli Header di risposta - non c’è nulla relativo alla cache e il browser obbedisce.

HTTP/1.1 200 OK
Content-Type: text/html
Date: <data>
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

Last-Modified - Endpoint

Specifica

Crea un nuovo endpoint (da registrare all’url /last-modified). Legge il tempo di modifica del file (mtime) e lo aggiunge formattato come UTC sotto l’header Last-Modified.

import { getView, getViewStats } from "./utils.mjs";

export default async (req, res) => {
  res.setHeader("Content-Type", "text/html");

  const [stats, errStats] = await getViewStats("index");
  if (errStats) {
	res.writeHead(500).end("Errore interno del server");
	return;
  }

  const lastModified = new Date(stats.mtime);
  res.setHeader("Last-Modified", lastModified.toUTCString());

  const [html, errGet] = await getView("index");
  if (errGet) {
	res.writeHead(500).end("Errore interno del server");
	return;
  }

  res.writeHead(200).end(html);
};

Infatti, tra gli header di risposta a /last-modified, trovi:

HTTP/1.1 200 OK
Last-Modified: Gio, 15 Nov 2023 19:18:46 GMT

Comunque, se aggiorni la pagina, l’intera risorsa viene ancora scaricata.

Tuttavia qualcosa è cambiato - il browser ha trovato Last-Modified, quindi riutilizza il valore per l’header di richiesta If-Modified-Since. Il server riceve quel valore e, se la condizione non è vera (non modificato da allora), restituisce lo stato 304 Not Modified.

import { getView, getViewStats } from "./utils.mjs";

export default async (req, res) => {
  res.setHeader("Content-Type", "text/html");

  const [stats, _] = await getViewStats("index");

  const lastModified = new Date(stats.mtime);
  lastModified.setMilliseconds(0); // IMPORTANTE
  res.setHeader("Last-Modified", lastModified.toUTCString());

  const ifModifiedSince = new Headers(req.headers).get("If-Modified-Since");
  if (
	ifModifiedSince &&
	new Date(ifModifiedSince).getTime() >= lastModified.getTime()
  ) {
	res.writeHead(304).end();
	return;
  }

  // Questo viene fatto SOLO SE non era un 304!
  const [html, _] = await getView("index");

  res.writeHead(200, headers).end(html);
};

Secondo la specifica Last-Modified

Nota:

  • L’header di risposta Last-Modified viene sempre aggiunto, anche nel caso di 304 Not Modified.
  • L’header di richiesta if-modified-since potrebbe non essere presente - succede sicuramente al primo chiamata da un nuovo client.

Soprattutto, le date HTTP sono sempre espresse in GMT, mai in ora locale.

Quando si formatta una data usando toUTCString, potresti notare che la stringa risultante perde informazioni sui millisecondi. Tuttavia mtime mantiene una precisione di millisecondi - potrebbe avere qualche millisecondo in più rispetto al valore ricevuto dal client, che, dopo la formattazione, perde quei millisecondi.

Per garantire un confronto valido tra i due valori, diventa necessario rimuovere i millisecondi da mtime prima di effettuare il confronto.

lastModified.setMilliseconds(0);

Infine, richiedi la risorsa più volte. La scheda di rete degli strumenti per sviluppatori che mostra il documento memorizzato nella cache dopo il primo recupero. Ora, vai a aggiornare il file HTML. Poi chiedi al browser di aggiornare e aspettati di ricevere una risposta 200 OK. La scheda di rete degli strumenti per sviluppatori che mostra il browser che recupera nuovamente la risorsa dopo che è stata modificata.


È fondamentale riconoscere che la risposta 304 è costantemente più leggera della risposta 200. Oltre a ridurre il carico dati, contribuisce a un diminuzione del carico sul server. Questa ottimizzazione va oltre la semplice lettura di file HTML e può applicarsi a qualsiasi operazione complessa o che richiede molte risorse.

Last-Modified è un header di caching debole, poiché il browser applica una euristica per determinare se recuperare l’elemento dalla cache oppure no. Le euristiche variano tra i browser.