Tre fonti, tre categorie di rischio diverse

L'idea era semplice e coerente con il resto del sito: zero costi server, zero chiavi da gestire dove possibile, dati letti direttamente dal browser. La pagina doveva mostrare la foto astronomica del giorno e immagini dei rover su Marte, la posizione in tempo reale della Stazione Spaziale Internazionale, e i prossimi lanci con dati su razzi e booster riutilizzabili — tre categorie, tre provider diversi, tutti documentati come "API pubblica gratuita, nessuna registrazione complessa".

Ho dato per scontato che "pubblica e gratuita" volesse dire anche "richiamabile direttamente da una pagina web". Non è così per definizione, ed è la prima cosa che è saltata fuori una volta pubblicata la pagina.

CORS bloccato: quando il problema non è nel codice

La sezione lanci restituiva un errore in console tanto chiaro quanto inutile a prima vista: No 'Access-Control-Allow-Origin' header is present on the requested resource. Il dettaglio importante è che quel controllo non lo fa il server dell'API — lo fa il browser di chi sta visitando il sito. Se il server non include quell'header nella risposta, il browser scarta la risposta prima ancora di passarla al codice JavaScript, anche se i dati richiesti sono arrivati regolarmente. Una richiesta identica fatta da terminale funziona senza intoppi, perché CORS è una regola che esiste solo nel contesto di un browser.

La soluzione standard è spostare la chiamata lato server: una piccola Netlify Function che interroga l'API di terze parti e restituisce la risposta con gli header CORS corretti verso il proprio dominio. Il browser dell'utente parla solo con la propria function, non con l'API remota, quindi il controllo CORS lato browser semplicemente non si applica più a quella parte del percorso.

⚠️

Non basta, quasi mai, fermarsi qui. Ho scritto il proxy, l'ho pubblicato, e la sezione ha continuato a restituire errori — solo con un codice diverso.

Il proxy funziona, ma la risposta resta un 502

Con il proxy attivo il blocco CORS era sparito, ma le richieste tornavano con un 502 Bad Gateway: la function contattava correttamente l'API remota, ma quella non rispondeva in modo utilizzabile. Prima di continuare a fare debug sul mio codice, ho controllato lo stato del progetto open source che alimentava quell'API. Il repository su GitHub risultava archiviato da circa un mese — in sola lettura, nessun nuovo commit possibile. Nel frattempo alcuni endpoint erano stati silenziosamente migrati a una nuova versione, mentre altri erano rimasti sulla precedente, e l'infrastruttura dietro l'API mostrava segnali di connettività compromessa, non solo di lentezza.

Un repository archiviato è un segnale forte quanto un changelog: nessuno sta più raccogliendo issue, nessuno sta più facendo deploy di fix. Continuare a fare debug contro un servizio in quello stato è tempo speso a rincorrere un bersaglio che nel frattempo può smettere di esistere del tutto.

Sostituire la fonte, non solo il codice

La scelta più solida, a quel punto, non era un altro giro di patch ma cambiare fornitore. Ho trovato un'API di tracciamento lanci mantenuta in modo professionale e attivo, con una struttura dati diversa ma sovrapponibile: stessa capacità di elencare lanci futuri e passati, stessa possibilità di recuperare configurazioni di razzo e — dettaglio che non davo per scontato — anche il tracciamento dei singoli booster riutilizzabili, con numero di serie e stato, l'equivalente esatto di quello che offriva l'API dismessa.

Prima di scrivere qualsiasi riga di codice ho verificato lo schema dei dati con una chiamata reale, non fidandomi della sola documentazione: è stato un controllo di dieci minuti che ha evitato un altro giro di "pubblico, testo, l'utente mi segnala l'errore, correggo".

Un 404 che in realtà è un cartello "chiuso per sempre"

In parallelo, la sezione con le foto dei rover su Marte aveva smesso di restituire risultati: l'endpoint principale rispondeva 404, e anche l'endpoint di fallback che avevo scritto proprio per quel tipo di situazione rispondeva 404 a sua volta. A quel punto il sospetto non era più "richiesta malformata" ma "il servizio non esiste più", e la conferma è arrivata da un posto che non avevo controllato per primo: il catalogo dati ufficiale collegato a quell'API riportava testualmente che il dataset non conteneva alcun dato, mantenuto da un singolo sviluppatore esterno, fermo da oltre un anno.

La differenza pratica tra un 404 isolato e un 404 strutturale sta proprio qui: un errore isolato su un solo endpoint, magari intermittente, è spesso un problema temporaneo. Più endpoint correlati che restituiscono lo stesso errore in modo costante, confermati da una fonte indipendente come un catalogo dati ufficiale, raccontano una storia diversa — un servizio abbandonato, non un disservizio.

Un rimpiazzo che cambia leggermente cosa promette la pagina

Al posto del servizio dismesso ho collegato la libreria immagini ufficiale dello stesso ente, un'infrastruttura diversa e attivamente mantenuta — ma con una natura del contenuto leggermente diversa: da "l'ultima foto scattata dal rover nelle ultime ore" a "immagini rilevanti dall'archivio ufficiale, filtrate per rover e ordinate per data". Stesse foto reali, stessa fonte istituzionale, ma non più una fotografia dell'istante esatto.

La tentazione, in questi casi, è di far finta che il cambiamento non ci sia e presentare comunque la sezione come "foto più recenti". Ho preferito scriverlo in modo esplicito accanto al selettore, con una frase che spiega perché non sono più le foto più recenti in senso stretto: un limite dichiarato onestamente è molto meno un problema di un risultato inspiegabile che l'utente scopre da solo.

Un bug più silenzioso: quando i dati strutturati non dicono la verità

Nell'ultimo passaggio di controllo ho trovato due problemi che non avevano niente a che fare con le API esterne. Il primo, semplice: la meta description superava abbondantemente i circa 155-160 caratteri che Google mostra prima di troncare in modo poco leggibile nei risultati di ricerca. Il secondo era più subdolo: tre delle sei coppie domanda/risposta nei dati strutturati FAQPage avevano una formulazione leggermente diversa rispetto al testo effettivamente visibile nella pagina — differenze nate scrivendo i due blocchi in momenti separati, invisibili a una rilettura veloce.

I motori di ricerca si aspettano che i dati strutturati FAQPage rispecchino fedelmente quello che un visitatore vede davvero, non un riassunto o una parafrasi: uno scarto anche minimo può rendere il contenuto non idoneo al rich result. L'ho trovato con un piccolo script che confronta programmaticamente le due versioni parola per parola, non rileggendo a occhio — lo stesso principio per cui vale la pena automatizzare un controllo invece di fidarsi della propria attenzione, specialmente su un testo che si è già letto troppe volte per notarne davvero le differenze.

Cosa mi porto a casa

La lezione più utile non riguarda un singolo errore, ma un ordine in cui verificare le cose: una chiamata reale batte sempre la documentazione, soprattutto per un progetto mantenuto dalla community; un proxy risolve il CORS ma non garantisce l'uptime del servizio a valle, sono due problemi distinti anche se si manifestano nello stesso punto del codice; un repository archiviato è già una risposta, non serve aspettare che l'API smetta anche formalmente di rispondere; e un dato strutturato va confrontato col contenuto visibile in modo automatico, perché a occhio le piccole differenze di formulazione sfuggono quasi sempre.

Se ti interessa lo stesso pattern di proxy server-side applicato a un caso diverso, ne ho parlato anche a proposito del riassunto PDF con Gemini Flash-Lite via Netlify Function. E se il tema è più in generale l'integrazione di un'API esterna in un progetto vanilla JS, c'è anche l'articolo sul chatbot con Gemini API nel gestionale.

Domande frequenti

Perché una chiamata fetch() a un'API pubblica può essere bloccata anche se l'API funziona correttamente?

Perché il blocco lo decide il browser di chi visita il sito, non il server dell'API. Se manca l'header Access-Control-Allow-Origin, il browser scarta la risposta prima di consegnarla al codice, anche se i dati sono arrivati regolarmente. Una richiesta identica da terminale funziona, perché CORS è una regola che esiste solo nel contesto browser.

Un proxy lato server risolve sempre un problema di CORS?

Risolve il blocco del browser, perché la richiesta parte dal server e non è soggetta alle stesse regole. Ma se il servizio remoto è lento o irraggiungibile, il proxy restituirà comunque un errore, solo con un codice diverso. È la soluzione corretta per il CORS, non una garanzia di uptime.

Come si capisce se un'API pubblica gratuita è stata abbandonata?

Segnali affidabili: il repository open source risulta archiviato o senza commit da tempo; un eventuale catalogo dati ufficiale riporta esplicitamente l'assenza di dati aggiornati; più endpoint correlati restituiscono lo stesso errore in modo costante, non intermittente.

Un errore 404 su un'API significa sempre che il servizio è stato dismesso?

No, spesso significa solo che una risorsa specifica non esiste in quel momento. Diventa un segnale di dismissione quando si verifica su più endpoint correlati in modo sistematico, inclusi i fallback, e trova conferma in fonti indipendenti come la documentazione ufficiale.

Perché i dati strutturati FAQPage devono corrispondere al testo visibile in pagina?

Perché i motori di ricerca li usano per generare rich result e si aspettano che rappresentino fedelmente ciò che un visitatore vede davvero. Anche una differenza minima nel testo rischia di rendere il contenuto non idoneo al rich result — meglio verificarlo con un controllo automatico che a occhio.