Skip to content

Anatomia di una transazione HTTP

Lo scopo di questa guida è fornire una solida comprensione del processo di gestione HTTP di Node.js. Assumeremo che tu conosca, in generale, come funzionano le richieste HTTP, indipendentemente dal linguaggio o dall'ambiente di programmazione. Assumeremo anche una certa familiarità con gli EventEmitters e gli Stream di Node.js. Se non hai familiarità con essi, vale la pena dare una rapida lettura alla documentazione dell'API per ognuno di essi.

Creare il server

Qualsiasi applicazione web server Node avrà a un certo punto bisogno di creare un oggetto web server. Questo si fa usando createServer.

javascript
const http = require('node:http')
const server = http.createServer((request, response) => {
  // la magia accade qui!
})

La funzione passata a createServer viene chiamata una volta per ogni richiesta HTTP effettuata su quel server, quindi è chiamata gestore di richieste. Infatti, l'oggetto Server restituito da createServer è un EventEmitter, e quello che abbiamo qui è solo un modo abbreviato per creare un oggetto server e poi aggiungere l'ascoltatore in seguito.

javascript
const server = http.createServer()
server.on('request', (request, response) => {
  // lo stesso tipo di magia accade qui!
})

Quando una richiesta HTTP raggiunge il server, Node chiama la funzione gestore di richieste con alcuni oggetti utili per gestire la transazione, la richiesta e la risposta. Ci arriveremo a breve. Per effettivamente servire le richieste, il metodo listen deve essere chiamato sull'oggetto server. Nella maggior parte dei casi, tutto ciò che dovrai passare a listen è il numero di porta su cui desideri che il server ascolti. Ci sono anche altre opzioni, quindi consulta il riferimento dell'API.

Metodo, URL e intestazioni

Quando si gestisce una richiesta, la prima cosa che probabilmente vorrai fare è guardare il metodo e l'URL, in modo che possano essere intraprese azioni appropriate. Node.js rende questo relativamente semplice mettendo delle proprietà utili sull'oggetto richiesta.

javascript
const { method, url } = request

L'oggetto richiesta è un'istanza di IncomingMessage. Il metodo qui sarà sempre un metodo/verbo HTTP normale. L'url è l'URL completo senza il server, il protocollo o la porta. Per un URL tipico, questo significa tutto dopo e compresa la terza barra.

Le intestazioni non sono nemmeno lontane. Sono nel loro stesso oggetto sulla richiesta chiamato headers.

javascript
const { headers } = request
const userAgent = headers['user-agent']

È importante notare qui che tutte le intestazioni sono rappresentate solo in minuscolo, indipendentemente da come il client le ha effettivamente inviate. Questo semplifica il compito di analizzare le intestazioni per qualsiasi scopo.

Se alcune intestazioni vengono ripetute, i loro valori vengono sovrascritti o uniti come stringhe separate da virgole, a seconda dell'intestazione. In alcuni casi, questo può essere problematico, quindi è disponibile anche rawHeaders.

Corpo della Richiesta

Quando si riceve una richiesta POST o PUT, il corpo della richiesta potrebbe essere importante per la vostra applicazione. Ottenere i dati del corpo è un po' più complesso rispetto all'accesso alle intestazioni della richiesta. L'oggetto richiesta passato a un handler implementa l'interfaccia ReadableStream. Questo stream può essere ascoltato o inviato altrove proprio come qualsiasi altro stream. Possiamo prendere i dati direttamente dallo stream ascoltando gli eventi 'data' e 'end' dello stream.

Il chunk emesso in ogni evento 'data' è un Buffer. Se sapete che si tratta di dati di stringa, la cosa migliore da fare è raccogliere i dati in un array, quindi, alla fine, concatenarli e convertirli in stringa.

javascript
let body = []
request.on('data', chunk => {
  body.push(chunk)
})
request.on('end', () => {
  body = Buffer.concat(body).toString()
  // a questo punto, 'body' contiene l'intero corpo della richiesta memorizzato come stringa
})

NOTA

Questo potrebbe sembrare un po' noioso, e in molti casi lo è. Fortunatamente, ci sono moduli come concat-stream e body su npm che possono aiutare a nascondere parte di questa logica. È importante avere una buona comprensione di ciò che sta accadendo prima di intraprendere questa strada, ed è per questo che siete qui!

Una Piccola Nota sugli Errori

Poiché l'oggetto richiesta è un ReadableStream, è anche un EventEmitter e si comporta come tale quando si verifica un errore.

Un errore nello stream di richiesta si presenta emettendo un evento 'error' sullo stream. Se non si ha un listener per quell'evento, l'errore verrà sollevato, il che potrebbe causare il crash del vostro programma Node.js. Dovreste quindi aggiungere un listener 'error' ai vostri stream di richiesta, anche se vi limitate a registrarlo e continuare. (Anche se probabilmente è meglio inviare una qualche tipo di risposta di errore HTTP. Maggiori informazioni in seguito.)

javascript
request.on('error', err => {
  // Questo stampa il messaggio di errore e la traccia dello stack su stderr.
  console.error(err.stack)
})

Ci sono altri modi per gestire questi errori, come altre astrazioni e strumenti, ma siate sempre consapevoli che gli errori possono e accadono, e dovrete gestirli.

Cosa abbiamo finora

A questo punto, abbiamo trattato la creazione di un server e l'estrazione del metodo, dell'URL, delle intestazioni e del corpo dalle richieste. Mettendo tutto insieme, potrebbe assomigliare a questo:

javascript
const http = require('node:http');

http.createServer((request, response) => {
    const { headers, method, url } = request;
    let body = [];
    request.on('error', err => console.error(err));
    request.on('data', chunk => {
        body.push(chunk);
    });
    request.on('end', () => {
        body = Buffer.concat(body).toString();
        // A questo punto, abbiamo le intestazioni, il metodo, l'URL e il corpo, e possiamo ora
        // fare tutto ciò che è necessario per rispondere a questa richiesta.
    });
});

.listen(8080); // Attiva questo server, in ascolto sulla porta 8080.

Se eseguiamo questo esempio, saremo in grado di ricevere richieste, ma non di rispondere ad esse. Infatti, se si prova questo esempio in un browser web, la richiesta si esaurirebbe, poiché nulla viene inviato al client.

Finora non abbiamo toccato per niente l'oggetto risposta, che è un'istanza di ServerResponse, che è un WritableStream. Contiene molti metodi utili per inviare dati al client. Lo vedremo nel prossimo capitolo.

Codice di stato HTTP

Se non ci si preoccupa di impostarlo, il codice di stato HTTP su una risposta sarà sempre 200. Naturalmente, non tutte le risposte HTTP lo giustificano e a un certo punto si vorrà sicuramente inviare un codice di stato diverso. Per fare ciò, è possibile impostare la proprietà statusCode.

javascript
response.statusCode = 404 // Comunica al client che la risorsa non è stata trovata.

Ci sono alcune altre scorciatoie per questo, come vedremo presto.

Impostazione delle intestazioni di risposta

Le intestazioni vengono impostate tramite un metodo conveniente chiamato setHeader.

javascript
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')

Quando si impostano le intestazioni su una risposta, il caso è insensibile ai loro nomi. Se si imposta un'intestazione ripetutamente, l'ultimo valore impostato è il valore che viene inviato.

Invio esplicito dei dati dell'intestazione

I metodi per impostare le intestazioni e il codice di stato che abbiamo già discusso presuppongono che si stia utilizzando "intestazioni implicite". Ciò significa che si conta sul fatto che Node.js invii le intestazioni al momento giusto prima di iniziare a inviare i dati del corpo.

Se lo si desidera, è possibile scrivere esplicitamente le intestazioni nel flusso di risposta. Per fare ciò, esiste un metodo chiamato writeHead, che scrive il codice di stato e le intestazioni nel flusso.

Invio esplicito dei dati dell'intestazione

javascript
response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon',
})

Una volta impostate le intestazioni (implicitamente o esplicitamente), è possibile iniziare a inviare i dati di risposta.

Invio del corpo della risposta

Poiché l'oggetto di risposta è un WritableStream, scrivere un corpo di risposta al client è solo una questione di utilizzare i soliti metodi di flusso.

javascript
response.write('<html>')
response.write('<body>')
response.write('<h1>Hello, World!</h1>')
response.write('</body>')
response.write('</html>')
response.end()

La funzione end sui flussi può anche accettare alcuni dati facoltativi da inviare come ultimo bit di dati sul flusso, quindi possiamo semplificare l'esempio sopra come segue.

javascript
response.end('<html><body><h1>hello,world!</h1></body></html>')

NOTA

È importante impostare lo stato e le intestazioni prima di iniziare a scrivere blocchi di dati nel corpo. Questo ha senso, poiché le intestazioni vengono prima del corpo nelle risposte HTTP.

Un'altra cosa veloce sugli errori

Il flusso di risposta può anche emettere eventi 'error', e a un certo punto sarà necessario gestirli anche. Tutti i consigli per gli errori del flusso di richiesta si applicano ancora qui.

Mettiamo tutto insieme

Ora che abbiamo imparato a creare risposte HTTP, mettiamo tutto insieme. Basandoci sull'esempio precedente, creeremo un server che reinvia tutti i dati inviati dall'utente. Formatteremo questi dati come JSON usando JSON.stringify.

javascript
const http = require('node:http')
http
  .createServer((request, response) => {
    const { headers, method, url } = request
    let body = []
    request
      .on('error', err => {
        console.error(err)
      })
      .on('data', chunk => {
        body.push(chunk)
      })
      .on('end', () => {
        body = Buffer.concat(body).toString()
        // INIZIO DELLE NUOVE COSE
        response.on('error', err => {
          console.error(err)
        })
        response.statusCode = 200
        response.setHeader('Content-Type', 'application/json')
        // Nota: le 2 righe sopra potrebbero essere sostituite con questa successiva:
        // response.writeHead(200, {'Content-Type': 'application/json'})
        const responseBody = { headers, method, url, body }
        response.write(JSON.stringify(responseBody))
        response.end()
        // Nota: le 2 righe sopra potrebbero essere sostituite con questa successiva:
        // response.end(JSON.stringify(responseBody))
        // FINE DELLE NUOVE COSE
      })
  })
  .listen(8080)

Esempio di EchoServer

Semplifichiamo l'esempio precedente per creare un semplice echo server, che semplicemente rispedisce i dati ricevuti nella richiesta nella risposta. Tutto ciò che dobbiamo fare è prendere i dati dal flusso di richiesta e scrivere quei dati nel flusso di risposta, simile a quanto fatto in precedenza.

javascript
const http = require('node:http');

http.createServer((request, response) => {
    let body = [];
    request.on('data', chunk => {
        body.push(chunk);
    });
    request.on('end', () => {
        body = Buffer.concat(body).toString();
        response.end(body);
    });
});

.listen(8080);

Ora modifichiamo questo. Vogliamo inviare un echo solo nelle seguenti condizioni:

  • Il metodo di richiesta è POST.
  • L'URL è /echo.

In qualsiasi altro caso, vogliamo semplicemente rispondere con un 404.

javascript
const http = require('node:http')
http
  .createServer((request, response) => {
    if (request.method === 'POST' && request.url === '/echo') {
      let body = []
      request
        .on('data', chunk => {
          body.push(chunk)
        })
        .on('end', () => {
          body = Buffer.concat(body).toString()
          response.end(body)
        })
    } else {
      response.statusCode = 404
      response.end()
    }
  })
  .listen(8080)

NOTA

Controllando l'URL in questo modo, stiamo facendo una forma di "routing". Altre forme di routing possono essere semplici come istruzioni switch o complesse come interi framework come express. Se stai cercando qualcosa che faccia il routing e nient'altro, prova router.

Ottimo! Ora proviamo a semplificare questo. Ricorda, l'oggetto request è un ReadableStream e l'oggetto response è un WritableStream. Ciò significa che possiamo usare pipe per indirizzare i dati dall'uno all'altro. Questo è esattamente ciò che vogliamo per un echo server!

javascript
const http = require('node:http')

http
  .createServer((request, response) => {
    if (request.method === 'POST' && request.url === '/echo') {
      request.pipe(response)
    } else {
      response.statusCode = 404
      response.end()
    }
  })
  .listen(8080)

Evviva gli stream!

Non abbiamo ancora finito però. Come menzionato più volte in questa guida, gli errori possono e accadono, e dobbiamo gestirli.

Per gestire gli errori sul flusso di richiesta, registreremo l'errore su stderr e invieremo un codice di stato 400 per indicare una Bad Request. In un'applicazione reale, tuttavia, vorremmo ispezionare l'errore per capire qual è il codice di stato e il messaggio corretti. Come al solito con gli errori, dovresti consultare la documentazione sugli errori.

Sulla risposta, registreremo semplicemente l'errore su stderr.

javascript
const http = require('node:http')

http
  .createServer((request, response) => {
    request.on('error', err => {
      console.error(err)
      response.statusCode = 400
      response.end()
    })
    response.on('error', err => {
      console.error(err)
    })
    if (request.method === 'POST' && request.url === '/echo') {
      request.pipe(response)
    } else {
      response.statusCode = 404
      response.end()
    }
  })
  .listen(8080)

Abbiamo ora coperto la maggior parte delle basi della gestione delle richieste HTTP. A questo punto, dovresti essere in grado di:

  • Istanza un server HTTP con una funzione di gestione delle request, e farlo ascoltare su una porta.
  • Ottenere intestazioni, URL, metodo e dati del corpo dagli oggetti request.
  • Prendere decisioni di routing in base all'URL e/o ad altri dati negli oggetti request.
  • Inviare intestazioni, codici di stato HTTP e dati del corpo tramite oggetti response.
  • Incanalare i dati dagli oggetti request e verso gli oggetti response.
  • Gestire gli errori di flusso sia nei flussi request che response.

Da queste basi, possono essere costruiti server HTTP Node.js per molti casi d'uso tipici. Queste API forniscono molte altre cose, quindi assicurati di leggere attentamente la documentazione dell'API per EventEmitters, Streams e HTTP.