CodeGym /Cours /Go SELF /http.NewRequestWithContext : en-tetes, query params

http.NewRequestWithContext : en-tetes, query params

Go SELF
Niveau 58, Leçon 2
Disponible

1. http.NewRequestWithContext et le role du contexte

Si vous debutez tout juste, il est facile de penser : « Pourquoi aurais-je besoin de ce NewRequestWithContext alors qu'il y a http.Get(url) ? ». Et en effet : http.Get, c'est comme « commander une pizza en un seul bouton ». Mais des que vous avez besoin de timeouts, d'en-tetes, d'une methode non standard ou d'une construction d'URL previsible, ce « seul bouton » se transforme en piege.

En Go, le contexte (context.Context) est devenu le standard pour gerer la duree de vie d'une operation, et net/http l'a integre justement dans l'objet requete. Cela a ete fait pour ne pas casser la compatibilite : Client.Do acceptait historiquement *http.Request, et le contexte a « demenage » a l'interieur de la structure de requete, et non dans la signature de Do.

Idee cle : vous creez un *http.Request, vous y « accrochez » le contexte, les en-tetes et l'URL, puis vous l'envoyez via client.Do(req).

Contexte : deadline et annulation

On essaie souvent de percevoir le contexte comme « quelque chose pour le multithreading » (et oui, il en a aussi besoin), mais dans un client HTTP il concerne d'abord les bornes temporelles et l'annulation de l'operation. Le contexte transporte un deadline/timeout et un signal d'annulation ; il peut aussi transporter des donnees « request-scoped » (mais il faut le faire avec prudence).

Au niveau quotidien, on peut imaginer cela ainsi : une requete est une enveloppe que vous envoyez par la poste. Le contexte est un minuteur : « si ce n'est pas arrive en 700 ms, on arrete de chercher le facteur dans toute la ville » et un bouton « annuler » si l'utilisateur change d'avis.

Mini-exemple : creation d'une requete avec timeout au niveau du contexte.

package main

import (
	"context"
	"net/http"
	"time"
)

func buildReq(url string) (*http.Request, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 700*time.Millisecond)
	defer cancel()

	return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}

Remarquez la discipline du defer cancel() : meme si la requete reussit, l'annulation libere les ressources associees au contexte (timers, etc.). Ce n'est pas une « superstition », mais une bonne habitude.

Pourquoi le contexte « vit » a l'interieur de Request

Si vous trouvez etrange que client.Do ne prenne pas ctx comme parametre distinct, c'est une reaction normale. Dans la bibliotheque standard de Go, il a ete decide d'integrer le contexte dans http.Request pour des raisons de compatibilite, afin que l'ancien code ne casse pas. Donc la bonne approche consiste toujours a creer la requete via NewRequestWithContext, pour que le contexte soit utilise « pendant toute la duree de vie de la requete ».

C'est une bonne decision d'ingenierie : vous « assemblez » la requete une fois (URL, en-tetes, contexte), puis elle se comporte de maniere previsible.

2. Query params : on ne concatene pas des chaines, on construit avec net/url

Les parametres de query sont la partie de l'URL apres ?, par exemple :

/api/v1/tasks?done=true&limit=10

Les debutants font souvent cela :

url := base + "/api/v1/tasks?done=" + done + "&limit=" + limit

Et cela fonctionne… jusqu'au premier espace, esperluette, point d'interrogation dans la valeur, ou jusqu'au moment ou vous avez un parametre contains="Go & coffee", et vous obtenez soudain une URL que le serveur lira comme « contains = Go » et « coffee = (quoi ?) ».

En Go, il y a net/url pour cela. Il fait deux choses importantes : il analyse/compose correctement l'URL et il echappe correctement les valeurs.

Base + path + query (sans magie ni douleur)

Il est pratique de construire une URL quand vous avez une adresse API de base et un ensemble de parametres. Dans le cadre de notre domaine pedagogique, nous allons imaginer que nous avons une Task API avec un endpoint :

  • GET /api/v1/tasks — liste des taches, filtrable.

Redigeons une petite fonction qui construit l'URL de la liste des taches a partir d'une URL de base et ajoute la query.

package main

import (
	"net/url"
	"strconv"
)

func buildListTasksURL(base string, done *bool, limit int) (string, error) {
	u, err := url.Parse(base)
	if err != nil {
		return "", err
	}
	u.Path = "/api/v1/tasks"

	q := u.Query()
	if done != nil {
		if *done {
			q.Set("done", "true")
		} else {
			q.Set("done", "false")
		}
	}
	if limit > 0 {
		q.Set("limit", strconv.Itoa(limit))
	}
	u.RawQuery = q.Encode()

	return u.String(), nil
}

Il y a ici plusieurs details subtils qui influent directement sur la qualite de votre code.

Premiereement, u.Query() renvoie une copie des valeurs de query (type url.Values), et pour que les modifications reviennent dans l'URL, nous assignons explicitement u.RawQuery = q.Encode().

Deuxiemement, q.Encode() trie lui-meme les cles et echappe tout correctement. C'est agreable : moins de travail manuel, moins de bugs.

3. Creer la requete comme objet : methode + URL + body

Une fois l'URL construite, il est temps de creer la requete. Et c'est ici que nous utilisons enfin le « heros de la serie » — http.NewRequestWithContext.

Il faut bien comprendre la signature :

http.NewRequestWithContext(ctx, method, url, body)

Dans cette construction, body est un io.Reader. Autrement dit, ce n'est ni une « chaine » ni des « octets », mais une source de donnees dont net/http lira le corps de la requete (si celui-ci est necessaire).

Exemple : requete GET pour la liste des taches

package main

import (
	"context"
	"net/http"
)

func newListTasksRequest(ctx context.Context, fullURL string) (*http.Request, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
	if err != nil {
		return nil, err
	}
	return req, nil
}

Pourquoi passons-nous un body nil ? Parce qu'un GET n'a en general pas de corps (cela peut exister, mais c'est tres rare et presque toujours une mauvaise idee dans une API applicative).

4. En-tetes : Accept, Content-Type, User-Agent

Les en-tetes sont des metadonnees de la requete. Si les query params sont des « annotations sur l'enveloppe » (visibles directement dans l'adresse), alors les headers sont des « autocollants de service » que lit le bureau de poste : ce qu'il y a dedans, quel format, qui doit etre considere comme expediteur, et ce que nous voulons recevoir en reponse.

Dans une API JSON, vous rencontrerez le plus souvent les en-tetes suivants :

En-tete Exemple Pourquoi
Accept
application/json
« Je veux recevoir une reponse en JSON »
Content-Type
application/json
« J'envoie un corps en JSON » (necessaire s'il y a un body)
User-Agent
tasks-cli/1.0
Identification du client (utile pour les logs et le diagnostic)
Authorization
Bearer <token>
ou
ApiKey <key>
Autorisation (si l'API l'exige)
X-Request-ID
...
Correlation des requetes dans les logs (souvent dans les microservices)

En Go, les en-tetes se definissent via req.Header.Set(key, value).

Accept et Content-Type : ce qu'ils signifient vraiment

Cette confusion est si frequente qu'on peut la considerer comme « l'initiation a HTTP ».

Accept concerne la reponse : « quel format de reponse je comprends ».

Content-Type concerne la requete : « dans quel format j'envoie le corps de la requete ».

Un GET sans body requiert en general seulement Accept.

package main

import "net/http"

func addJSONAccept(req *http.Request) {
	req.Header.Set("Accept", "application/json")
}

Un POST/PUT/PATCH avec un corps JSON requiert en general a la fois Accept et Content-Type.

package main

import "net/http"

func addJSONHeaders(req *http.Request) {
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
}

Si vous oubliez Content-Type lors de l'envoi de JSON, le serveur (s'il est strict) peut dire : « Je ne comprends pas ce que tu m'as apporte, mon ami ».

User-Agent : une petite chaine qui fait gagner des heures

Cela semble anodin : « pourquoi ce User-Agent ? ». Mais lorsque vous avez des logs serveur et cinq clients differents (CLI, tests, integration, navigateur, service voisin), User-Agent devient un filtre rapide : qui fait des requetes etranges et pourquoi.

Redigeons un petit helper :

package main

import "net/http"

func addUserAgent(req *http.Request, ua string) {
	if ua == "" {
		return
	}
	req.Header.Set("User-Agent", ua)
}

Si ua est vide, nous ne definissons pas l'en-tete. C'est plus pratique que de definir une « vacuite par defaut ».

5. Construire la requete completement : query + headers

Assemblons maintenant tout cela dans une fonction propre qui prend le contexte, l'URL et les parametres du client, puis renvoie un *http.Request pret a l'emploi.

Remarquez : nous n'envoyons pas la requete, nous la construisons seulement. C'est une bonne decomposition : construire et executer sont des responsabilites differentes.

package main

import (
	"context"
	"net/http"
)

func buildListTasksReq(ctx context.Context, fullURL string, userAgent string) (*http.Request, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Accept", "application/json")
	addUserAgent(req, userAgent)

	return req, nil
}

Et c'est ici que le style Go devient perceptible : de petites fonctions-briques qui font chacune une chose claire.

Impression de debogage : comprendre rapidement ce qui a ete « mal assemble »

Situation tres typique : le serveur renvoie 400/404, et vous etes sur que tout est correct. Jusqu'au moment ou vous imprimez l'URL finale et voyez que vous avez obtenu :

  • ...?done=true%20 (espace),
  • ou vous avez oublie d'assigner RawQuery,
  • ou Path a « mange » une partie du chemin.

Mini-exemple : impression des parties cles de la requete (sans envoi).

package main

import (
	"fmt"
	"net/http"
)

func debugReq(req *http.Request) {
	fmt.Println("method:", req.Method)       // method: GET
	fmt.Println("url:", req.URL.String())    // url: https://...
	fmt.Println("accept:", req.Header.Get("Accept"))
}

C'est du « printf-debugging » au sens le plus noble : peu couteux, rapide, efficace.

6. Corps JSON et Content-Type

Bien que le theme de la lecon soit les en-tetes et les query params, un cas « classique » merite tout de meme d'etre montre : comment Content-Type est lie a NewRequestWithContext lorsque vous envoyez un JSON-body.

Imaginons un POST /api/v1/tasks avec un corps { "title": "..." }.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
)

type createTaskRequest struct {
	Title string `json:"title"`
}

func buildCreateTaskReq(ctx context.Context, fullURL, title string) (*http.Request, error) {
	b, err := json.Marshal(createTaskRequest{Title: title})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(b))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	return req, nil
}

Ici, ce qui importe, c'est que bytes.NewReader(b) est justement un io.Reader, celui que NewRequestWithContext attend. Et l'en-tete Content-Type devient une « promesse » faite au serveur : « je t'ai envoye du JSON ».

7. Schema : comment la requete est construite logiquement

Quand tout est melange dans une seule fonction de 50 lignes, le cerveau se brouille : ou est l'URL, ou est le timeout, ou sont les en-tetes. Il est donc utile de garder un schema mental simple.

flowchart TD
    A["Parametres de fonction : baseURL, filtres, ctx"] --> B["Construire l'URL : net/url + query.Encode()"]
    B --> C["Creer Request : http.NewRequestWithContext"]
    C --> D["Definir les en-tetes : Accept, User-Agent, Content-Type..."]
    D --> E["Envoyer : client.Do(req)"]

Ce schema indique a la fois l'ordre des operations et les points ou il est pratique d'insrer une impression de debogage.

8. Erreurs typiques

Erreur n°1 : construire la query par concaténation de chaines.
Au debut, cela semble plus rapide : on ajoute "?a=" + v. Mais des que la valeur contient un espace, &, ?, = ou des caracteres non ASCII, l'URL commence a « se casser » et le serveur lit quelque chose de tout a fait different. net/url resout cela par l'echappement et une construction correcte.

Erreur n°2 : modifier q := u.Query() et oublier d'assigner u.RawQuery.
q.Set(...) donne l'impression de modifier directement l'URL, mais en pratique, sans u.RawQuery = q.Encode(), les changements resteront « dans la variable locale ». Resultat : la requete part sans parametres, et ensuite vous passez une demi-heure a discuter avec le moniteur.

Erreur n°3 : confondre Accept et Content-Type.
Lorsque vous mettez Content-Type: application/json sur un GET sans body, ce n'est en general pas grave, mais c'est etrange. En revanche, ne pas mettre Content-Type sur un POST avec un corps JSON est une vraie cause de refus cote serveur. Rappelez-vous : Accept concerne la reponse, Content-Type concerne le corps de la requete.

Erreur n°4 : creer une requete sans contexte et esperer que « ca ira bien comme ca ».
Une requete sans contexte est techniquement possible (http.NewRequest), mais alors vous oubliez plus souvent les deadlines et l'annulation. Et les requetes sans timeout dans le code reseau, c'est comme « je sors une minute » sans preciser de quel pays et pour combien de semaines.

Erreur n°5 : definir les en-tetes apres client.Do(req).
Parfois, les debutants pensent qu'on peut modifier les en-tetes « a la volee », mais la requete est deja partie. Les en-tetes et l'URL doivent etre entierement construits avant l'execution de Do, sinon vous modifiez un objet en memoire et non ce qui est parti sur le reseau.

1
Mission
Go SELF, niveau 58, leçon 2
Bloqué
Brouillon de requête
Brouillon de requête
1
Mission
Go SELF, niveau 58, leçon 2
Bloqué
Lien de recherche
Lien de recherche
1
Mission
Go SELF, niveau 58, leçon 2
Bloqué
Liste des tâches
Liste des tâches
1
Mission
Go SELF, niveau 58, leçon 2
Bloqué
Création d'une tâche
Création d'une tâche
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION