Lezione 2 di 7 · 20 min di lettura

Il primo server

Un server web vero in quindici righe. I pacchetti mist e wisp, il gestore delle richieste, parlare con il server dal browser e da curl, fermarlo, e cosa succede se la porta è già occupata.

Due pacchetti, due mestieri

Per scrivere un server web in Gleam servono due pacchetti, che fanno due lavori diversi:

  • mist è il server HTTP: apre la porta, accetta le connessioni, legge i byte che arrivano e li trasforma in un valore Request, e poi rispedisce indietro la Response come testo HTTP. È la parte “idraulica”.
  • wisp è un framework web: non parla con la rete, ma ti dà gli strumenti per scrivere la parte che decide cosa rispondere. Risposte già pronte (wisp.ok(), wisp.not_found()…), lettura di moduli e JSON, protezioni di sicurezza, log.

Tu scrivi una funzione che riceve una richiesta e restituisce una risposta; wisp ti aiuta a scriverla, mist la collega al mondo. Dalla cartella exercises (il pacchetto gleam_erlang c’è già dal modulo 7):

terminale
gleam add wisp mist

gleam add scarica anche le loro dipendenze: una ventina di pacchetti in tutto, tra cui glisten (su cui si appoggia mist per le connessioni TCP) e gleam_http, che hai già aggiunto nella lezione precedente.

Quindici righe

Crea src/hello_server.gleam:

src/hello_server.gleam
import gleam/erlang/process
import mist
import wisp
import wisp/wisp_mist

pub fn main() -> Nil {
  let secret_key_base = wisp.random_string(64)
  let assert Ok(_) =
    wisp_mist.handler(handle_request, secret_key_base)
    |> mist.new
    |> mist.port(8000)
    |> mist.start
  process.sleep_forever()
}

fn handle_request(_request: wisp.Request) -> wisp.Response {
  wisp.html_response("<h1>Hello from Gleam!</h1>", 200)
}

Partiamo dal fondo, perché è la parte più importante. handle_request è il gestore (handler): una normale funzione che riceve una wisp.Request e restituisce una wisp.Response. Per ora ignora la richiesta (per questo il parametro si chiama _request, lezione 2.1) e risponde sempre con la stessa pagina HTML e il codice 200. wisp.html_response imposta anche l’intestazione content-type: text/html; charset=utf-8.

Poi main, riga per riga:

  1. wisp.random_string(64) genera una chiave segreta casuale. Wisp la usa per firmare i dati che affida al browser, come i cookie: per questo esempio non serve, ma la vuole comunque.
  2. wisp_mist.handler(handle_request, chiave) è l’adattatore tra i due pacchetti: trasforma il tuo gestore wisp in un gestore che mist sa usare.
  3. mist.new prepara un server con quel gestore, mist.port(8000) sceglie la porta, mist.start lo avvia. Avviare può fallire, quindi mist.start restituisce un Result, che controlliamo con let assert (lezione 5.4): se il server non parte, non ha senso proseguire.
  4. process.sleep_forever() addormenta main per sempre. Ricordi la lezione 7.2? Quando main finisce, il programma si spegne con tutti i suoi processi, server compreso. Un server deve restare acceso, e così main non finisce mai.
Dettagli nerd Cosa vuol dire firmare un dato?

Un server a volte affida delle informazioni al browser, per esempio “questo utente ha fatto l’accesso”, dentro un cookie: un piccolo dato che il browser conserva e rimanda a ogni richiesta. Il problema è che il browser è nelle mani dell’utente, che può cambiare il cookie come vuole.

La firma risolve il problema: il server calcola, a partire dal dato e dalla chiave segreta, una specie di impronta digitale, e la attacca al dato. Quando il cookie torna indietro, il server ricalcola l’impronta: se non coincide, qualcuno l’ha modificato. Senza la chiave non si può produrre un’impronta valida, ed è per questo che deve restare segreta. Una chiave casuale a ogni avvio va bene per provare; in un sito vero si usa sempre la stessa, conservata fuori dal codice, altrimenti a ogni riavvio i cookie firmati prima smettono di valere.

Accendere il server

terminale
gleam run -m hello_server
output
Listening on http://127.0.0.1:8000

E poi… niente. Il terminale non torna al prompt, e va bene così: il server è acceso, e aspetta. 127.0.0.1 è localhost (lezione precedente), e la riga la stampa mist quando la porta è aperta.

Apri il browser all’indirizzo http://localhost:8000. Vedrai il titolo “Hello from Gleam!”: il browser ha mandato una richiesta GET /, mist l’ha passata al tuo gestore, e la risposta è tornata indietro. Il tuo primo sito web.

Parlare con il server da curl

Il browser è comodo, ma nasconde i dettagli. Per vederli c’è curl, un programma che fa richieste HTTP dal terminale e stampa quello che torna: c’è già su Linux, macOS e Windows 10 e successivi. Il server occupa il primo terminale, quindi aprine un secondo:

terminale
curl localhost:8000
output
<h1>Hello from Gleam!</h1>

Senza opzioni, curl fa una richiesta GET e stampa solo il corpo. Con -i (include) mostra anche la prima riga e le intestazioni della risposta:

terminale
curl -i localhost:8000/anything
output
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 26
date: Mon, 28 Sep 2026 13:42:20 GMT
connection: keep-alive

<h1>Hello from Gleam!</h1>

Ecco la risposta HTTP della lezione precedente, costruita dal tuo codice. Il content-type l’ha messo wisp.html_response; content-length, date e connection li aggiunge mist. E nota il percorso: /anything riceve la stessa pagina, perché il gestore non guarda la richiesta. Anche curl -X POST localhost:8000/x (con -X scegli il metodo) risponde uguale. Nella prossima lezione il gestore imparerà a distinguere.

Spegnere il server

Torna nel primo terminale e premi Ctrl+C. È il menu di interruzione della BEAM della lezione 2.6:

output
BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
       (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution

Scrivi a e premi Invio. Il server si spegne, e da quel momento curl localhost:8000 risponde con un errore: non c’è più nessuno ad ascoltare sulla porta 8000.

Ogni volta che cambi il codice, devi spegnere il server e riaccenderlo: gleam run compila e avvia il programma, ma non si accorge dei file che cambiano mentre gira.

La porta è occupata

Una porta può avere un solo programma in ascolto. Se provi ad avviare il server una seconda volta, mentre il primo è ancora acceso (in un altro terminale, o perché ti sei dimenticato di spegnerlo), il secondo non riesce ad aprirla. L’uscita è lunga, perché a lamentarsi sono i supervisori interni di mist (lezione 7.6); la riga che conta è questa:

output
Failed to start socket listener: Eaddrinuse

Eaddrinuse è il nome in codice del sistema operativo per address in use, “indirizzo già in uso”. Il rimedio è spegnere l’altro server, oppure scegliere un’altra porta con mist.port.

Quiz

Perché main finisce con process.sleep_forever()?

Esercizio · sul tuo computer

Un secondo server

Crea src/second_server.gleam: un server sulla porta 8080 che risponde a qualsiasi richiesta con il testo semplice Server number two, at your service (non HTML). Per il testo semplice c’è wisp.ok(), una risposta 200 già pronta, e wisp.string_body(risposta, testo) per cambiarne il corpo.

Avvialo con gleam run -m second_server, e da un secondo terminale chiedi curl localhost:8080. Cosa stampa? Poi prova curl -i localhost:8080, e guarda che content-type ha messo wisp.ok().

Mostra una soluzione (prima prova da solo!)
src/second_server.gleam
import gleam/erlang/process
import mist
import wisp
import wisp/wisp_mist

pub fn main() -> Nil {
  let secret_key_base = wisp.random_string(64)
  let assert Ok(_) =
    wisp_mist.handler(handle_request, secret_key_base)
    |> mist.new
    |> mist.port(8080)
    |> mist.start
  process.sleep_forever()
}

fn handle_request(_request: wisp.Request) -> wisp.Response {
  wisp.ok()
  |> wisp.string_body("Server number two, at your service")
}

Con curl -i vedrai content-type: text/plain: è quello che mette wisp.ok(), e string_body cambia il corpo senza toccare le intestazioni. Il server della lezione, sulla porta 8000, può restare acceso nello stesso momento: porte diverse, nessun conflitto.

Ricapitolando

  • mist è il server HTTP (connessioni, byte, protocollo); wisp è il framework con cui scrivi le risposte. gleam add wisp mist.
  • Un gestore è una funzione fn(wisp.Request) -> wisp.Response.
  • wisp_mist.handler(gestore, chiave_segreta) lo adatta a mist; mist.new, mist.port, mist.start avviano il server.
  • process.sleep_forever() tiene acceso main, e quindi il programma.
  • wisp.html_response(html, 200) risponde con una pagina; wisp.ok() |> wisp.string_body(testo) con testo semplice.
  • curl fa richieste dal terminale: -i mostra le intestazioni, -X sceglie il metodo, -d manda dati.
  • Si spegne con Ctrl+C, poi a. Dopo ogni modifica al codice, va riavviato.
  • Eaddrinuse: la porta è già occupata da un altro programma.

Nella prossima lezione il server smetterà di dare sempre la stessa risposta: guarderemo percorso e metodo, e impareremo a provare il gestore senza accendere il server.