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.
Three sources, three different categories of risk
The idea was simple and consistent with the rest of the site: zero server costs, zero keys to manage where possible, data read straight from the browser. The page needed to show the astronomy picture of the day and Mars rover images, the International Space Station's real-time position, and upcoming launches with rocket and reusable-booster data — three categories, three different providers, all documented as "free public API, no complicated signup".
I assumed "public and free" also meant "callable directly from a web page." That's not true by definition, and it was the first thing that surfaced once the page went live.
CORS blocked: when the problem isn't in your code
The launches section returned a console error that was as clear as it was unhelpful at first glance: No 'Access-Control-Allow-Origin' header is present on the requested resource. The key detail is that this check isn't done by the API's server — it's done by the browser of whoever is visiting the site. If the server doesn't include that header in the response, the browser discards the response before it ever reaches the JavaScript code, even though the requested data arrived just fine. An identical request made from a terminal works without a hitch, because CORS is a rule that only exists in a browser's context.
The standard fix is to move the call server-side: a small Netlify Function that queries the third-party API and returns the response with the correct CORS headers pointed at your own domain. The user's browser only talks to your own function, not to the remote API, so the browser-side CORS check simply no longer applies to that part of the path.
Almost never enough to stop there. I wrote the proxy, deployed it, and the section kept returning errors — just with a different code.
The proxy works, but the response is still a 502
With the proxy in place the CORS block was gone, but requests came back with a 502 Bad Gateway: the function was reaching the remote API correctly, but it wasn't responding usefully. Before digging further into my own code, I checked the status of the open source project powering that API. The GitHub repository had been archived about a month earlier — read-only, no new commits possible. In the meantime some endpoints had silently migrated to a new version while others stayed on the old one, and the infrastructure behind the API showed signs of broken connectivity, not just slowness.
An archived repository is as strong a signal as a changelog entry: nobody is triaging issues anymore, nobody is shipping fixes. Continuing to debug against a service in that state is time spent chasing a target that might stop existing entirely in the meantime.
Replacing the source, not just the code
The sturdier choice at that point wasn't another round of patches but switching providers. I found a launch-tracking API that's professionally and actively maintained, with a different but comparable data shape: the same ability to list future and past launches, the same access to rocket configurations, and — a detail I hadn't taken for granted — tracking of individual reusable boosters too, with serial number and status, the exact equivalent of what the discontinued API offered.
Before writing a single line of code I verified the data schema with a real live call, instead of trusting the documentation alone: a ten-minute check that avoided another round of "ship it, someone reports the bug, fix it".
A 404 that actually means "closed for good"
In parallel, the Mars rover photos section had stopped returning results: the main endpoint responded 404, and even the fallback endpoint I'd written for exactly this situation responded 404 too. At that point the suspicion shifted from "malformed request" to "the service no longer exists," and the confirmation came from a place I hadn't checked first: the official data catalog linked to that API stated in plain text that the dataset contained no data, maintained by a single external developer, untouched for over a year.
The practical difference between an isolated 404 and a structural one sits right here: a single error on one endpoint, maybe intermittent, is often a temporary hiccup. Multiple related endpoints returning the same error consistently, confirmed by an independent source like an official data catalog, tell a different story — an abandoned service, not a service outage.
A replacement that slightly changes what the page promises
In place of the discontinued service I connected the same agency's official image library, a different, actively maintained piece of infrastructure — but with a slightly different content nature: from "the latest photo the rover took in the last few hours" to "relevant images from the official archive, filtered by rover and sorted by date." Same real photos, same institutional source, but no longer a snapshot of the exact moment.
The temptation in these cases is to pretend nothing changed and keep presenting the section as "latest photos." I chose to state it explicitly next to the selector instead, with a line explaining why these aren't strictly the most recent photos anymore: an honestly disclosed limitation is far less of a problem than an unexplained result the user has to figure out on their own.
A quieter bug: when structured data doesn't tell the truth
In the final review pass I found two problems that had nothing to do with the external APIs. The first was simple: the meta description comfortably exceeded the roughly 155-160 characters Google shows before truncating awkwardly in search results. The second was sneakier: three of the six FAQPage structured-data question/answer pairs had slightly different wording than the text actually visible on the page — differences born from writing the two blocks in separate passes, invisible on a quick re-read.
Search engines expect FAQPage structured data to faithfully mirror what a visitor actually sees, not a summary or a paraphrase: even a small mismatch can make the content ineligible for the rich result. I found it with a small script that programmatically compares the two versions word for word, not by eyeballing it — the same reason it's worth automating a check instead of trusting your own attention, especially on text you've already read too many times to notice the differences.
What I take away from this
The most useful lesson isn't about a single error, but about an order in which to verify things: a real call always beats documentation, especially for a community-maintained project; a proxy fixes CORS but doesn't guarantee the uptime of the service downstream — two separate problems even when they show up at the same point in the code; an archived repository is already an answer, no need to wait for the API to formally stop responding too; and structured data needs to be checked against visible content automatically, because small wording differences almost always slip past a manual read.
If you're interested in the same server-side proxy pattern applied to a different case, I also wrote about summarizing PDFs with Gemini Flash-Lite via a Netlify Function. And if the topic is more generally about integrating an external API into a vanilla JS project, there's also the article on the Gemini API chatbot inside the management dashboard.
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.
Frequently asked questions
Why can a fetch() call to a public API be blocked even if the API works correctly?
Because the block is decided by the visiting browser, not the API's server. If the Access-Control-Allow-Origin header is missing, the browser discards the response before handing it to the code, even if the data arrived fine. An identical request from a terminal works, because CORS only exists in a browser context.
Does a server-side proxy always fix a CORS problem?
It fixes the browser block, since the request originates from the server and isn't subject to the same rules. But if the remote service is slow or unreachable, the proxy will still return an error, just with a different code. It's the correct fix for CORS, not a guarantee of uptime.
How can you tell if a free public API has been abandoned?
Reliable signals: the open source repository behind it is archived or hasn't seen commits in a long time; an official data catalog, if one exists, explicitly states there's no updated data; multiple related endpoints return the same error consistently, not intermittently.
Does a 404 error on an API always mean the service was discontinued?
No, it often just means a specific resource doesn't exist at that moment. It becomes a discontinuation signal when it happens systematically across multiple related endpoints, including fallbacks, and is confirmed by an independent source like official documentation.
Why does FAQPage structured data need to match the visible page text?
Search engines use it to generate rich results and expect it to faithfully represent what a visitor actually sees. Even a small text mismatch can make content ineligible for the rich result — better to check it automatically than by eye.