Ultima Modifica / Se Modificato Da

Ultima Modifica / Se Modificato Da

Scritto da Francesco Di Donato 16 novembre 2023 4 minuti di lettura

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.