Utiliser le code de statut de réponse “103 Early Hints” dans les applications Go

Read the original version of this post.

103 est un nouveau code de statut HTTP expérimental défini dans la RFC 8297. Il s'agit d'un statut informationnel qui peut être envoyé par un serveur avant la réponse HTTP principale. Utilisé en conjonction avec l'en-tête HTTP Link et la relation preload, 103 donne au client la possibilité de récupérer des ressources (assets, images, documents liés à l’API...) liées à celles explicitement demandées, le plus tôt possible, alors que le serveur prépare la réponse principale. Early Hints ressemble à cela :

HTTP/1.1 103 Early Hints
Link: ; rel=preload; as=style
Link: ; rel=preload; as=script

HTTP/1.1 200 OK
Date: Fri, 26 May 2017 10:02:11 GMT
Content-Length: 1234
Content-Type: text/html; charset=utf-8
Link: ; rel=preload; as=style
Link: ; rel=preload; as=script

[… le reste de la réponse est volontairement retiré de cet exemple …]

Early Hints en quelques mots

Le code de statut 103 Early Hints pourrait être une bonne alternative au Server Push d’HTTP/2, qui sera supprimé de Chrome dans un futur proche et déconseillé dans la spécification d’HTTP/2. Il ajoute 1 Round Trip Time (RTT) par rapport au Server Push, mais, parmi d'autres avantages, il permet une meilleure mise en cache et est (théoriquement) plus facile à implémenter. Chrome et Fastly mènent une expérience pour mesurer l'avantage potentiel de ce nouveau code de statut. Mais pour que cette expérience soit couronnée de succès, nous avons besoin de serveurs compatibles... et les serveurs attendent des navigateurs compatibles avant de mettre en œuvre ce nouveau code de statut. Nous sommes dans un problème typique de "l'œuf ou la poule". 

Go et les Early Hints

Comme vous le savez peut-être, chez Les-Tilleuls.coop, nous sommes attachés au sujet du préchargement des ressources appliqué aux APIs web. Nous sommes à l’origine du protocole Vulcain, qui est une alternative à (certaines fonctionnalités de) GraphQL. Il permet de concevoir des APIs rapides et idiomatiques, orientées client, en suivant strictement le style architectural REST. Vulcain supporte Early Hints depuis le départ, et a été conçu en tenant compte de la compatibilité avec ce code de statut. Nous publierons bientôt une nouvelle révision de Vulcain prenant en compte le déclin du Server Push, et Kévin Dunglas présentera en détails ce que cela changera pour le protocole lors de l'AFUP Day Lille 2021.

Cependant, le serveur de passerelle Vulcain (l'implémentation de référence), ne supporte pas encore le nouveau code de statut. Les serveurs utilisant ce composant ne peuvent donc pas participer à l'expérience.

Comme le hub Mercure.rocks, le serveur de passerelle Vulcain est maintenant disponible en tant que module du serveur web Caddy. Cela a été rendu possible parce que Caddy et la bibliothèque qui implémente Vulcain sont tous les deux écrits en Go. Malheureusement, la bibliothèque standard de Go ne supporte pas encore le code de statut 103. Cela empêche d'utiliser Early Hints avec Caddy et Vulcain.

Afin d’avancer sur le sujet, nous avons soumis au projet Go des patchs mettant en œuvre la RFC pour HTTP/1.1 et pour HTTP/2. Ils ne sont pas encore mergés, mais comme la version 1.16 de Go a été publiée il y a quelques jours, cela pourrait bientôt atterrir dans la branche de développement. En attendant, il est déjà possible d'utiliser cette fonctionnalité dans vos propres projets Go, et c'est ce que nous verrons dans la suite de cet article !

La chaîne d'outils Go (en particulier le compilateur gc) a une caractéristique intéressante : elle crée des binaires liés statiquement par défaut. Cela signifie qu'une fois compilés avec une version de développement de Go supportant la nouvelle fonctionnalité, vos binaires autonomes peuvent être déployés sans devoir changer quoi que ce soit sur vos serveurs.

Assurez-vous d'abord que la version stable de Go est installée. Comme la chaîne d'outils de Go elle-même est écrite en Go, nous avons besoin de Go pour compiler Go. C'est ce qu'on appelle le processus de bootstrapping (encore une histoire "d’œuf ou la poule").

Ensuite, clonez notre fork de Go et allez sur la branche contenant les changements requis pour HTTP/1.1 :


git clone https://github.com/dunglas/go.git dunglas-go
cd dunglas-go
git checkout feat/http-103-status-code

Cette branche contient le patch pour HTTP/1.1 mais pas pour HTTP/2. L'implémentation HTTP/2 de Go est stockée dans un module séparé : x/net/http2. Avant de créer notre version personnalisée de Go, nous devons récupérer la version corrigée de x/net/http2 et la regrouper dans la bibliothèque standard. Commencez par remplacer le paquet x/net par notre fork, et mettez à jour les dépendances vendored :

export GOROOT=$(pwd)
cd src/
go mod edit -replace="golang.org/x/net=github.com/dunglas/net@2f6bd1bb0ddb1202d12f52cd0bd643d4eeedf1ce"
go mod vendor
unset GOROOT

Ensuite, installez la commande bundle, et utilisez-la pour regrouper le module net/http

go get golang.org/x/tools/cmd/bundle
cd net/http/
$(go env GOPATH)/bin/bundle -o=h2_bundle.go -dst net/http -prefix=http2 -tags='!nethttpomithttp2' golang.org/x/net/http2

Le fichier h2_bundle.go a été remplacé par un bundle contenant notre patch.

Enfin, retournez dans le répertoire src/ et construisez la version Go adaptée au code de statut 103

cd ../../
./make.bash

Notre compilateur Go “amélioré” est prêt !

Exemple de programme 

La mise en œuvre de l'exemple fourni dans la RFC est simple :

// main.go
package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    helloHandler := func(w http.ResponseWriter, req *http.Request) {
        w.Header().Add("Link", "; rel=preload; as=style")
        w.Header().Add("Link", "; rel=preload; as=script")

        w.WriteHeader(103)

        // do your heavy tasks such as DB or remote APIs calls here

        w.WriteHeader(200)

        io.WriteString(w, "\n[... le reste de la réponse est volontairement retiré de cet exemple ...]")
    }

    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
}

Pour générer un certificat TLS approuvé localement (nécessaire pour utiliser HTTP/2), nous recommandons la commande mkcert

mkcert -cert-file ./cert.pem -key-file ./key.pem localhost

Utilisez le build personnalisé de Go que nous avons créé pour compiler le programme, et lancez-le : 

/path/to/dunglas-go/bin/go build main.go
./main main

Vous pouvez aussi exécuter go run main.go pour compiler et démarrer le programme avec une seule commande. Utilisez curl pour vérifier s'il fonctionne correctement. 

Avec HTTP/1.1 :

curl -v --http1.1 https://localhost/hello

et avec HTTP/2 :

 curl -v https://localhost/hello

Vous devriez voir quelque chose comme ceci :

[snip]
< HTTP/2 103
< link: ; rel=preload; as=style
< link: ; rel=preload; as=script
< HTTP/2 200
< link: ; rel=preload; as=style
< link: ; rel=preload; as=script
< content-type: text/html; charset=utf-8
< content-length: 79
< date: Sat, 13 Feb 2021 09:47:27 GMT
<

[snip]

Envoi d’Early Hints 

Appeler plusieurs fois http.ResponseWriter.WriteHeader() provoquera une erreur avec l’exécution Go vanilla. Pour pouvoir compiler le même programme avec le compilateur stable et avec celui qui est patché, vous pouvez “envelopper” l'appel à http.ResponseWriter.WriteHeader(103) dans une condition :

package main

import (
    "io"
    "log"
    "net/http"
    "runtime"
    "strings"
)

func main() {
    helloHandler := func(w http.ResponseWriter, req *http.Request) {
        w.Header().Add("Link", "; rel=preload; as=style")
        w.Header().Add("Link", "; rel=preload; as=script")

        if strings.HasPrefix(runtime.Version(), "devel") {
            // skip if compiled with a stable version of Go
            w.WriteHeader(103)
        }

        w.WriteHeader(200)

        io.WriteString(w, "\n[... le reste de la réponse est volontairement retiré de cet exemple ...]")
    }

    http.HandleFunc("/hello", helloHandler)

    log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
}

103 Early Hints et HTTP/3

HTTP/3 est la prochaine version du leader des protocoles web. Il devrait prochainement devenir une RFC. HTTP/3 est déjà activé par défaut dans Safari et il peut être activé manuellement dans Firefox et Chrome.

Une implémentation expérimentale de HTTP/3 pour Go (qui est utilisée par Caddy et Vulcain) est disponible, mais elle ne prend pas en charge le code de statut 103. Une pull request a été ouverte pour ajouter le support de ce code de statut à quic-go. Pour utiliser le code de statut 103 avec HTTP/3 : essayez ce patch

Et voilà ! Vous êtes prêt·e à créer des programmes supportant ce nouveau code de statut, et vous pouvez participer à cette expérience pour rendre le web plus rapide et plus vert ! Si vous vous en servez, prévenez-nous sur Twitter ! Le serveur de passerelle Vulcain sera bientôt mis à jour pour supporter le code de statut 103 en utilisant l'approche que nous avons vue dans cet article. Testez les patches, et prévenez-nous si vous rencontrez le moindre souci !