Informatica per script kiddies 13 – Le API REST e quel mondo lì

Eccoci con Informatica per script kiddies, che è arrivato al tredicesimo numero! Insomma, sì, nasceva un anno fa e non mi aspettavo di essere ancora qui a scrivere dopo un anno. Vi ringrazio per l’entusiasmo, che mi ha dato la voglia di continuare a scrivere questi articoli, e ringrazio u/Sudneo, che ha scritto l’ottavo numero permettendomi di riprendere un po’ il fiato (e che spero di rivedere presto, magari assieme ad altri, in questa rubrica).

Oggi parliamo di API REST, un argomento che vedo sempre più spesso in giro tra chi non ha una formazione strettamente informatica. I motivi trovo siano soprattutto due: il primo è la disponibilità sempre maggiore di dati open forniti in questa modalità, e il secondo è che il mondo dei siti web non-statici si è ormai massicciamente spostato verso un modello frontend/backend con i due “end” che comunicano proprio attraverso una API REST fornita dal backend. Sedetevi comodi, è lungo.

Intanto, cosa è una API

API è una sigla che sta per Application Programming Interface, “interfaccia di programmazione dell’applicazione”. Si tratta di un concetto molto vasto, ed estremamente più articolato ed esteso del piccolo sottoinsieme che riguarda il mondo di Internet e del web.

Una API è una qualunque interfaccia che permetta ad un’applicazione di utilizzare un’altra applicazione o programmarne il flusso di lavoro.

Le API che più comunemente si utilizzano programmando sono ai fatti le librerie: una libreria – ovvero un software specializzato per fornire funzionalità ad un altro software – si interfaccia con il programma che la usa attraverso un sistema di comandi che è a tutti gli effetti una API.

Allo stesso modo, il sistema operativo, che altro non è che un software che fornisce routine e servizi a tutti gli altri software che girano sulla macchina, viene utilizzato attraverso un sistema di API, le cosiddette “chiamate al sistema”.

E cos’è il web, invece

Ad un certo punto della storia dell’informatica ci si è ritrovati con grandi reti di tanti computer, fino ad arrivare alla rete enorme che è Internet. Su queste reti si sono cominciati a sviluppare servizi di comunicazione di ogni genere. Alcuni avevano origini già precedenti, e continuano a vivere ancora oggi godendo di ottima salute, ad esempio la posta elettronica. Altri sono stati stroncati da servizi più moderni, come IRC sostituito dalle chat, o i newsgroup sostituiti dal web (oggi la cosa più simile è Reddit, a suo modo). Uno, che sarebbe diventato una cosa enorme nonostante all’epoca ce lo si aspettasse poco, è il web.

Con il web abbiamo a che fare ogni giorno, al punto che non lo distinguiamo neanche troppo da Internet, ma è in realtà una cosa ben distinta: se Internet è una rete di computer, il web è una rete di siti. Se i computer di Internet sono collegati – grossomodo – da cavi, i siti sono collegati da link. Il web, insomma, nasce come insieme di siti collegati tra loro, formando i cosiddetti ipertesti (parola passata di moda solo perché diventata ovvia).

Alla base di tutta questa magia c’è un protocollo di comunicazione che permette di trasferire un ipertesto (una “pagina web” e le sue componenti) attraverso Internet, l’HTTP, e alcuni standard (ne vedremo uno, l’URL).

HTTP

HTTP, che sta per hypertext transfer protocol, “protocollo di trasferimento per ipertesti”, è (tutt’ora, nelle sue varianti moderne e cifrate) il protocollo che permette al web di esistere su Internet.

Si tratta di un sistema molto semplice e che è utile conoscere per capire meglio le REST API, quindi ne parliamo. Una comunicazione HTTP avviene in due fasi, la richiesta e la risposta.

La richiesta è la fase in cui un client web (ad esempio il browser) richiede a un server web (ad esempio quello che ospita il sito) una “risorsa”, ovvero un oggetto che gli interessa. La richiesta contiene alcuni elementi, che nel caso delle richieste cifrate vengono cifrati tutti tranne l’host:

  • L’host, ovvero l’indirizzo del server
  • L’indirizzo della risorsa
  • Il metodo, ovvero l’azione che il client vuole fare presso il server (ne parleremo)
  • Alcuni parametri di configurazione
  • Il contenuto, detto “corpo”, se necessario

La risposta è strutturata in maniera del tutto analoga, al netto di host e indirizzo della risorsa. Al posto dei parametri di configurazione, ci sono dei metadati sulla risposta (formato, lunghezza, eventuale specifica di errore…). Nel corpo della risposta è presente la risorsa richiesta dal client, se richiesta.

URL

L’URL, Uniform Resource Locator, “localizzatore uniforme di risorse”, è uno standard per assegnare un indirizzo ad ogni risorsa di un insieme di risorse. È estremamente universale (ci vuoi indirizzare i libri di una biblioteca? Lo sconsiglio, ma puoi) e molto articolato e complesso, ma lo vediamo nel sottoinsieme che ci interessa per il web.

Un indirizzo URL di una risorsa web, come siamo abituati, ha in generale questa forma: protocollo://host:porta/indirizzo_sul_server?parametro=valore&altro_parametro=altro_valore. La porta è facoltativa (il client userà quella specifica del protocollo, come 80 per l’http), e indirizzo e parametri si usano solo se utili. Ad esempio, l’indirizzo di questo articolo è qualcosa del tipo https://tldr.italyinformatica.org/2021/01/informatica-per-script-kiddies-13-le-api-rest/

https è il protocollo, tldr.italyinformatica.org è l’host, /2021/01/informatica-per-script-kiddies-13-le-api-rest/ è l’indirizzo della risorsa sul server. La porta è omessa (e quindi è la 443, propria del protocollo https) e i parametri sono superflui.

Il web dinamico

Il web, come abbiamo visto, è una roba semplice. Le robe semplici hanno un grosso problema: si finisce ad usarle per cose a cui si adattano bene, sia perché sono appunto semplici sia perché sono scarne, ma per le quali non sono state pensate neanche lontanamente. Al web è successo molto presto – al punto che in un certo senso il protocollo http è stato effettivamente pensato perché consentisse usi più ampi – ed è successo massicciamente.

I browser si sono evoluti velocemente – anche a causa della concorrenza tra i vari esistenti – per fare moltissime cose. In particolare, ad un certo punto hanno tutti in una certa forma iniziato a supportare JavaScript, un linguaggio di programmazione vero e proprio (all’inizio neanche tanto, ma…) per permettere di manipolare interattivamente le pagine web.

Il web ha a questo punto aggiunto un passaggio al suo funzionamento. Se prima il browser chiedeva una pagina, il server rispondeva, e quella pagina veniva poi consultata dall’utente, con JavaScript la risposta proveniente dal server conteneva anche il codice necessario a permettere all’utente stesso – o al browser – di modificare localmente la pagina senza interpellare il server. Insomma, le cose a cui siamo molto abituati oggi: premi un pulsante e una parte della pagina si modifica, con l’aggiunta o la rimozione di parti.

Il passaggio successivo è stato ovvio: iniziare a chiedere parti di pagina al server anche dopo che la pagina iniziale era stata caricata. Voglio mettere un riquadrotto con le previsioni meteo? Ogni tot tempo lo aggiorno, solo quello, chiedendo al server solo il riquadrotto (che avrà uno URL suo) senza richiedere per intero tutta la pagina.

Da qui in poi è stato tutto in discesa: si è andati rapidamente verso il punto in cui richiedere una “pagina web” è il semplice richiedere il codice JavaScript che la disegnerà attraverso una serie successiva di richieste che non scaricano neanche più i componenti (i “riquadrotti”), ma solo i dati (ad esempio tempo e temperatura).

I web service

Parallelamente a tutto questo, è nata appunto l’idea che le “risorse” non dovessero necessariamente essere pagine web, parti di pagine web e oggetti multimediali (immagini, video…), ma potessero essere anche – e ormai soprattutto – dati, nudi e crudi.

Il web non è stato pensato per trasferire dati, dicevamo appunto che è una rete di pagine linkate, ma i protocolli su cui si basa hanno effettivamente tutto il necessario, quindi perché no? Se la pagina web o un suo componente è utilizzabile solo per fare quella singola pagina web, il dato ha il vantaggio di poter essere usato per fare molte cose diverse: la stessa risorsa può essere usata per tante pagine web differenti, o per la versione “web” e la versione “app” dello stesso apparato.

Dopo una situazione di generale caos, nei primi anni duemila – grazie anche e soprattutto alla definizione di REST – sono nati i web service, ovvero dei sistemi basati sui protocolli del web ma atti a scambiare dati invece di cose che sono effettivamente web.

I sistemi per realizzare web service e gli standard per farli funzionare sono tantissimi. Alcuni hanno avuto grande successo e altri meno. Oggi il panorama è composto da molti sistemi più o meno codificati, ma quasi sempre basati su una di due grandi architetture o sulla loro eredità. REST, la più semplice e per questo diffusa, e RPC, più articolata, complessa e generale (trascende il mondo del web), ed estremamente potente e descrittiva, ma per questo anche più complessa (sebbene ci siano protocolli del mondo RPC, come SOAP, fortemente orientati al web e di complessità non eccessiva).

L’architettura REST

Un sistema di web service, per essere RESTful, ovvero “che risponde ai canoni delle architetture REST”, deve avere specifiche caratteristiche:

  • Essere client – server: devono esistere un client e un server che si occupino di compiti ben distinti. Trattandosi di un’architettura tipicamente usata per il web, ad esempio, il server deve occuparsi di trasmettere dati, mentre l’interfaccia è compito del client. Si viola questo principio se, ad esempio, assieme ai dati vengono inviati pezzi di interfaccia (il riquadrotto meteo che dicevamo).
  • Essere stratificabile: l’aggiunta di strati fisici (proxy, load balancer) o logici (autenticazione, sicurezza) non deve rompere il funzionamento del sistema. Inoltre, deve essere possibile che diversi server si occupino di generare la risposta finale che il client ottiene. In altre parole, il client non deve avere idea di come sia fatto il sistema interrogato: un client REST interroga risorse, non sistemi.
  • Essere cacheable: il client deve poter memorizzare, se opportuno, la risposta del server in una cache per utilizzi successivi. Le risposte del server devono quindi contenere, esplicitamente o da specifica, il loro tempo di validità, così che il client possa sapere per quanto tempo tenerle in cache prima di doverle richiedere nuovamente.
  • Essere stateless: le informazioni sullo “stato”, ovvero tutte le informazioni che non dipendono dai dati comunicati, devono essere mantenute dal client – e non dal server – e fornite al server quando necessarie. Se ad esempio una risorsa richiede particolari autorizzazioni per l’accesso, le informazioni di autenticazione devono essere mantenute sul client e inviate con la richiesta: non è compito del server sapere che le richieste provenienti da un certo client sono autorizzate.
  • Avere un’interfaccia uniforme, concetto abbastanza composito che possiamo riassumere in:
    • Ogni risorsa deve avere un suo identificatore univoco (ad esempio l’URL). L’identificatore deve identificare una singola risorsa e sempre quella.
    • La rappresentazione della risorsa non è la risorsa. Due rappresentazioni diverse della stessa risorsa (ad esempio in JSON e XML) hanno lo stesso identificatore, perché sono la stessa risorsa: il client può scegliere, se disponibili, rappresentazioni diverse attraverso parametri passati in richiesta.
    • Manipolare la rappresentazione della risorsa permette di manipolare la risorsa: se un client richiede una risorsa, ottiene la sua rappresentazione. Se ha l’autorità per modificarla, deve poter comunicare al server la versione modificata della risorsa comunicandogli la sua rappresentazione manipolata, è compito del server tradurla nell’effettivo formato in cui lui la mantiene.
    • Richieste e risposte devono essere auto-descrittive, specificando ad esempio il formato in cui sono inviate. Nulla deve essere inferito dal client o dal server “dando un’occhiata” al corpo della richiesta. Deve essere tutto specificato, o dalla specifica o da parametri.
    • Se in una risorsa sono contenuti riferimenti ad altre risorse, questi devono essere scritti in un formato che il client può usare direttamente per ottenerle. Se ad esempio una risorsa ottenuta via URL fa riferimento ad altre risorse, di queste deve essere specificato l’URL.

REST su HTTP

Generalmente REST si utilizza su protocollo HTTP, usando gli URL come identificatori di risorse.

I metodi

Nel paragrafo su HTTP ho introdotto il fatto che con la richiesta viene inviato un metodo. Il metodo è una stringa, un verbo in inglese (viene spesso chiamato proprio verbo invece che metodo), che specifica cosa il client vuole fare con la specifica risorsa.

I metodi di HTTP che REST utilizza sono un set abbastanza limitato. Vedremo solo i principali, ovvero GET, HEAD, POST, PUT, PATCH, DELETE e OPTIONS. Oltre alle funzioni specificherò tre caratteristiche molto importanti quando si manipolano risorse, ovvero:

  • Se il metodo è idempotente, ovvero se eseguire la richiesta con quel metodo per due volte consecutive ha lo stesso effetto che eseguirla una sola volta. Si tratta di una cosa fondamentale da sapere, perché permette di sapere se in caso di dubbio di errore sia sicuro o meno eseguirla una seconda volta.
  • Se il metodo è sicuro, ovvero se eseguire la richiesta con quel metodo non modifica alcuna risorsa.
  • Se la risposta alla richiesta eseguita con quel metodo è cacheable, ovvero se ha senso tenerla in cache o se è valida solo nel momento in cui viene data.

Vediamoli uno a uno, con una premessa: in REST su HTTP le risorse in genere sono o oggetti (http://host/people/123 è la persona numero 123) o collezioni di oggetti (http://host/people/ sono tutte le persone, o http://host/people/?surname=rossi sono le persone con cognome rossi). Quando parlo di “collezione” intendo questo.

GET

GET è il metodo più ovvio, serve ad ottenere una risorsa. È idempotente, perché chiedendo una risorsa due volte semplicemente la riottieni, è sicuro perché legge soltanto, ed è cacheable. Una richiesta GET contiene generalmente, assieme all’URL di una risorsa, il formato in cui la si vuole ottenere. La risposta conterrà la risorsa stessa (a meno di errori specificati), e una serie di informazioni a riguardo, tra cui generalmente il formato, la dimensione del corpo e la durata della validità in cache.

HEAD

HEAD è un metodo identico a GET, e quindi idempotente, sicuro e cacheable, che serve ad ottenere una risposta senza il corpo. La risposta conterrà quindi solo le informazioni aggiuntive, come l’eventuale errore, la dimensione e la validità della cache, ma non la risorsa. È utile ad esempio nei casi in cui si vuole sapere quanto spazio occupa la rappresentazione della risorsa prima di scaricarla effettivamente. Può anche essere usato per sapere semplicemente se la risorsa esiste quando non si è interessati al suo contenuto.

POST

POST è il metodo utilizzato per creare una nuova risorsa. Non è idempotente, poiché se lanciato due volte crea due risorse, non è sicuro poiché scrive sul server. La richiesta conterrà nel corpo la risorsa completa. La risposta, che potrebbe contenere nel corpo la risorsa creata (di solito è così), è cacheable. In genere una richiesta POST si farà su una collezione, poiché l’oggetto da creare, ovviamente, non esiste e non ha quindi un suo indirizzo. Una richiesta POST, dunque, ai fatti serve ad aggiungere una nuova risorsa a una collezione.

PUT

PUT è il metodo utilizzato per sostituire una risorsa presente con un una nuova risorsa, che prenderà il suo indirizzo ma può essere completamente diversa a livello di contenuti. Si tratta di un metodo idempotente, poiché se eseguito due volte sostituirà due volte la medesima risorsa con i medesimi contenuti, ma non è sicuro poiché scrive sul server. Come nel caso delle richieste POST la richiesta conterrà nel corpo la risorsa completa, diversamente dalla risposta che non la contiene e non è quindi cacheable. Le richieste PUT hanno senso anche su collezioni, purché contengano nel corpo i sostituti di una collezione di risorse.

PATCH

PATCH è un metodo molto simile a PUT ma con importanti differenze: serve a inviare un set di istruzioni per modificare parte di una risorsa, invece che sostituirla. Non è un metodo sicuro, poiché scrive, e non è un metodo cacheable, perché la risposta non contiene alcuna risorsa nel corpo. In generale non è neanche idempotente. Se di solito infatti per “le istruzioni” si intende una parte della risorsa (se devo solo cambiare il nome, invio solo il nome invece che reinviare tutti i dati della persona con una PUT), a seconda della specifica una PATCH potrebbe contenere ordini non idempotenti (il classico “incrementa di uno il contatore”). Insomma, bisogna fare attenzione.

DELETE

DELETE cancella una risorsa (o una collezione). Non è ovviamente sicuro, non è cacheable in quanto la risposta non contiene nulla di significativo nel tempo, ma è idempotente, poiché cancellare due volte la stessa risorsa non ha effetti diversi da cancellarla una sola volta (per quanto le chiamate successive alla prima restituiranno idealmente un errore).

OPTIONS

OPTIONS è un metodo banalmente idempotente, sicuro e non cacheable (la risposta non ha un corpo) che offre informazioni su quali impostazioni di configurazione (metodi, tipi di dato…) la risorsa chiamata supporta.

I formati

Sui formati di trasmissione dei dati ho parlato un bel po’ nello scorso articolo, ma vediamo un po’ cose più specifiche, soprattutto sulle richieste, che non sono sempre effettivamente dati.

Oggi il formato principe in ambito REST è il JSON, un formato nato con scopi abbastanza diversi dalla trasmissione dei dati, ma che si adatta piuttosto bene allo scopo. Le risposte di una API REST, quindi, sono quasi sempre in JSON o in un formato JSON-based. Talvolta, però, si incontrano formati diversi da questo, o è possibile richiederli alla API in alternativa al JSON. Il formato più diffuso assieme al JSON è l’XML (o un suo dialetto standardizzato), che è effettivamente concepito allo scopo e che è stato per molti anni l’unico formato utilizzato a questo fine (ne è rimasta traccia in giro, iconico il fatto che la funzione per fare richieste http in JavaScript si chiami ancora oggi XMLHttpRequest e che la tecnica si chiami ancora oggi AJAX, Asynchronous JavaScript and XML, anche quando l’XML non c’entra nulla).

Per le richieste, le cose si fanno più articolate, soprattutto quando la richiesta non trasporta dati.

Le richieste GET, HEAD, DELETE e OPTIONS non trasportano quasi mai informazioni nel corpo. Tutto quello che serve per la richiesta (parametri, tipicamente) è solitamente inserito nell’URL stesso, nella query string, la parte dedicata ai parametri. Eventuali dati di configurazione, come il formato in cui si desidera la risposta, sono inseriti nella testa della richiesta, nella parte appositamente dedicata.

Le richieste POST e PUT, che devono inviare la risorsa per intero, in genere utilizzano come formato uno dei formati che il server sa dare come risposta. Questo è sempre possibile per le richieste PUT: il requisito dell’uniformità prevede infatti che sia possibile modificare una risorsa semplicemente ottenendo la sua rappresentazione e modificando la rappresentazione stessa. Non si può quindi obbligare il client a usare una rappresentazione apposita per effettuare modifiche.

Le richieste PATCH, in genere, utilizzano come formato un sottoinsieme della struttura della risposta. Se ad esempio si deve modificare solo un singolo aspetto del dato, generalmente si può inviare una richiesta PATCH con solo quello. Le richieste PATCH, però, potrebbero anche supportare una sintassi completamente diversa che trasmetta istruzioni su come modificare un dato, senza inviare effettivi dati (“incrementa il contatore tale di uno”, senza dover necessariamente sapere quanto valeva prima, ad esempio).

Se il client vuole la risposta in un formato specifico, dovrà usare il parametro di configurazione Accept specificando il MIME del formato stesso (ad esempio Accept: application/json chiede al server un JSON).

I codici di stato

Il protocollo HTTP prevede che nella risposta, nell’intestazione, sia presente un codice numerico a tre cifre che dica se la richiesta è andata a buon fine o fallita, e in che modo. Il client dovrebbe sempre controllare tale codice, e agire di conseguenza se non è quello che si aspetta. Non andrò nel dettaglio dei codici esistenti, ma vale la pena elencare le loro categorie e come ci si comporta in genere di conseguenza.

Codici 1xx (informativi)

Se il codice è nel primo centinaio, sono di tipo informativo. Non capitano sostanzialmente mai nelle comunicazioni REST, tranne forse il 101 che dice al client – che ha chiesto di cambiare protocollo ad esempio da http a https – che il server ha recepito l’ordine. Sono codici che comunque ci si aspetta, quando arrivano.

Codici 2xx (successo)

I codici del secondo centinaio sono quelli che dicono che la richiesta è andata a buon fine. Se il codice è in questo centinaio, nella risposta ci troveremo ciò che effettivamente abbiamo chiesto. Quelli più interessanti per REST sono il 200, che indica che la richiesta è andata a buon fine e il 201 che indica la medesima cosa per le richieste di creazione di una nuova risorsa (POST).

Codici 3xx (redirezione)

Il terzo centinaio indica che la risorsa richiesta – se esiste, non è detto – va cercata “altrove”. I casi che capitano facilmente in REST sono il 301 che indica che la risorsa non è più lì ma va cercata all’indirizzo specificato nel parametro Location della risposta, il 302 che (erroneamente, ma) indica la medesima cosa ma temporanea, e il 304 utilizzato quando in una richiesta cacheable viene inviata, come parametro, la data dell’ultima versione che il client ha in cache: se la risposta è 304 non conterrà nel corpo la risorsa, indicando che la versione cache è ancora valida.

Codici 4xx (errore del client)

Il quarto centinaio indica che il client ha sbagliato qualcosa. I più comuni sono 400 che indica una richiesta formalmente errata (ad esempio contenente parametri non sensati), 401 che indica una richiesta a cui il server può rispondere se il client fosse autorizzato, ma il client non lo è, 403 che indica che la richiesta è formalmente corretta ma il server non può soddisfarla in nessun caso perché vietata, 404 che indica che la risorsa richiesta non esiste, 405 che indica che il metodo non è applicabile a quella risorsa (ad esempio il caso in cui si provi a fare POST su un oggetto esistente e non su una collezione), 406 che indica che il server non è in grado di rispondere nel formato richiesto (ad esempio se si chiede un XML a un server che risponde solo in JSON), 410 che indica che la risorsa non è più disponibile, 413 e 414 che indicano che rispettivamente l’URL o il corpo della richiesta sono troppo lunghi, e 422 che indica che la richiesta è formalmente corretta ma non lavorabile: in genere si utilizza quando il server si rifiuta di elaborare una richiesta valida formalmente ma non valida nell’ambito dell’applicativo (una data di nascita formalmente corretta ma futura per una persona già nata).

Insomma, è la parte più interessante e corposa (corposa anche perché contiene di tutto, oltre a questo sottoinsieme che ho elencato, compreso 418 che indica che il server non può rispondere con un caffè perché è una teiera), e in cui è meno facile districarsi. In generale in presenza di risposte con questi codici bisogna comportarsi con raziocinio. Inutile fare di nuovo una GET che risponde 404 o 410 ad esempio, mentre se il proprio client supporta più formati magari è il caso di fare OPTIONS a seguito di una 406 e vedere se tra i formati possibili se ne trova uno supportato.

Codici 5xx (errore del server)

Il quinto centinaio sono gli errori del server. I due grandi errori sono il 500, che indica che il server ha un generico errore e il 501, che indica che la funzionalità non è stata implementata. Molto importanti in ambito REST il 504, che indica un timeout, che può essere causato da un sovraccarico temporaneo o da una richiesta molto esosa, e il 509, non presente nelle specifiche di HTTP ma molto utilizzato, che indica che si sono superati i limiti di banda disponibili sul server.

In genere, dopo questi errori ha senso ritentare la richiesta se è una richiesta idempotente, o controllare se la richiesta ha avuto effetto o meno se non è idempotente.

Conclusioni

Credo di aver fatto una panoramica abbastanza esaustiva della questione, ma potrebbero essermi sfuggite cose, o potrei aver dato per scontato altre cose. Vi invito a chiedermi delucidazioni (o a darle) nel thread reddit di questo articolo su /r/ItalyInformatica.

Buon divertimento!