skip to content

How do you attach an X-Trace-Id header to an outbound *http.Request, and how does Header.Set differ from Header.Add?

level: juniorimportance: should knowfreq 60%

answer

  1. one name, a slice of values
  2. replace or append
  3. the key is rewritten before the map write
  4. Get reads only the first value
  5. Del is not Set with an empty string

basics

~20 s

Build the request, then call req.Header.Set("X-Trace-Id", id) before sending it. Set replaces every value already stored under that key; Add appends another value and keeps the earlier ones. Both store the key in canonical form.

solid answer

~40 s

`http.Header` is a `map[string][]string` with helper methods, so you set a trace header by calling `req.Header.Set("X-Trace-Id", id)` on the `*http.Request` after `http.NewRequestWithContext` returns it and before `client.Do` sends it. `Set` replaces whatever was stored under that key; `Add` appends, so the key ends up holding a slice — right for headers that may legitimately repeat, almost never right for a trace id. `Get` returns the first value, `Values` returns all of them, `Del` removes the key. All of those canonicalise the key first, so `Set("x-trace-id", …)` stores `X-Trace-Id`. Assigning straight into the map stores the key exactly as you typed it and skips that canonicalisation, which is how a header becomes invisible to code that reads it with `Get`.

code

go · 7 lines
go
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
	return err
}
req.Header.Set("X-Trace-Id", traceID)  // replaces any existing value
req.Header.Add("X-Debug-Tag", "retry") // appends alongside existing values
resp, err := http.DefaultClient.Do(req)

go deeper

for a junior

Be ready to write the lines cold: build the request, call Header.Set with the name and value, then send it. Know that Set replaces the value and Add appends another one.

for a middle

Explain that http.Header is a map from name to a slice of values, and that Set, Add, Get, Values and Del all rewrite the key into canonical form while a direct map write does not.

for a senior

Show judgment about which headers may legitimately repeat and which must be single-valued, and argue for setting outbound headers in one shared place rather than repeating the call at every site.

for a principal

Own the convention itself: which correlation header names your services agree on, whether the edge validates or regenerates them, and what a service does when a request arrives carrying none.

### What an http.Header is In `net/http`, `http.Header` is declared as `map[string][]string` with methods attached to it. The key is a header name; the value is a slice because HTTP permits the same header name to appear on more than one line. The same type is used on both sides: the `*http.Request` you build for an outbound call and the `*http.Request` your handler receives both expose it as `req.Header`. ### Setting a header on a request you are about to send The order is always the same: create the request, mutate its header, then hand it to a client. ```go req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return err } req.Header.Set("X-Trace-Id", traceID) resp, err := http.DefaultClient.Do(req) ``` Once `Do` has been called the request has been written; mutating the header afterwards changes nothing on the wire and, if the request is still in flight, is a data race. ### Set versus Add - `Set(key, value)` — the key ends up with exactly one value. Any values already there are discarded. - `Add(key, value)` — the value is appended to whatever slice is already under the key. For a trace or request identifier you want `Set`. Two values under a correlation header is not "more information", it is an ambiguity every downstream reader has to resolve, and readers that call `Get` will silently take the first one. `Add` earns its place for headers that are defined as lists — `Accept`, `Set-Cookie` on a response, forwarding headers that each hop appends to. ### The rest of the small API - `Get(key) string` — the first value, or `""` when the key is absent. It never errors and never panics. - `Values(key) []string` — every value stored under the key, `nil` when absent. - `Del(key)` — removes the key entirely. This is not the same as `Set(key, "")`, which leaves the key present with one empty value and still writes a header line. - `Clone()` — a deep copy of the map, useful when you want to hand a modified header set to something else without mutating the original. ### Canonicalisation, in one paragraph `Set`, `Add`, `Get`, `Values` and `Del` all run the key through the same canonicalisation before touching the map: the first letter and every letter following a hyphen is upper-cased and the rest are lower-cased, so `x-trace-id`, `X-TRACE-ID` and `x-Trace-ID` all address the single map key `X-Trace-Id`. That is why those five methods look case-insensitive. Direct map assignment — `req.Header["x-trace-id"] = []string{id}` — is an ordinary Go map write and does no such thing: the map now has a second, non-canonical key, `req.Header.Get("X-Trace-Id")` will not find it, and on an HTTP/1 connection your literal casing goes out on the wire. Use the methods unless you have a deliberate reason not to. ### Where the call belongs Setting the header at every call site is how a chain acquires a hole: one new outbound call is written, nobody remembers the line, and that hop starts a fresh trace. Route outbound requests through one constructor or one client-side helper that sets the header, so adding a call is not also a chance to forget it. ### Common mistakes Using `Add` where `Set` was meant, and only discovering it when a downstream service logs two identifiers. Assigning into the map and then wondering why `Get` returns `""`. Expecting `Set(key, "")` to delete. Mutating the header after `Do`. All four are cheap to avoid once you know the type is a plain map with five well-behaved methods on top.

  • What does req.Header.Get return when the header was never set?
    The empty string. `Get` canonicalises the key, looks it up and returns `""` when the key is missing or its slice is empty — it never errors and never panics. That also means `Get` cannot distinguish a header that was absent from one sent with an empty value; when that difference matters, check the length of `req.Header.Values(key)` instead.
  • An upstream helper already set a header you must remove. What do you call?
    `req.Header.Del("X-Trace-Id")`, which canonicalises the key and deletes it from the map. Setting it to the empty string is not equivalent: the key stays in the map with one empty value, and the header line is still written on the wire, so a receiver checking only for presence still sees it.
  • Is it safe to set headers on a request after passing it to client.Do?
    No. By the time `Do` returns, the request has been written; a later mutation changes nothing that was sent. Worse, while the call is in flight the transport may still be reading the header map, so mutating it concurrently is a genuine data race that the race detector will flag. Finish building the request before you send it.

Set is assigning to a variable; Add is appending to a list. The header map holds a list under every name, and the two methods differ only in whether they clear it first.

saying these in an interview costs you the question

  • Treats Add and Set as interchangeable for any header
  • Assigns into req.Header directly, then reads with Get
  • Thinks Set with an empty string removes the header
  • Believes Get matches header keys case-sensitively
  • Sets headers after the client has already sent the request