How to Send Custom HTTP Headers in Go
Set request and response headers correctly in Go with runnable examples, context, auth, trailers, troubleshooting, and a ScreenshotNeo shortcut.

Go’s standard library makes custom HTTP headers explicit: build an http.Request, set its headers, and send it with http.Client.Do. For a server response, set headers on http.ResponseWriter.Header() before writing the status or body. This distinction prevents the most common mistakes.
The examples below cover authentication, content negotiation, tracing, cookies, repeated headers, context cancellation, response headers, trailers, testing, performance, and failure handling. The patterns use only Go’s standard net/http package.
Set custom headers on an outgoing request
Use http.NewRequest (or http.NewRequestWithContext) when you control the client request. Set one value with Header.Set, append another value with Header.Add, then call Client.Do.

package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/v1/orders", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Request-ID", "order-list-123")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("API returned %s: %s", resp.Status, body))
}
fmt.Println(string(body))
}
The Go documentation recommends this request-construction workflow for custom headers: use NewRequest and Client.Do. A successful Do call only means the exchange completed; it does not mean the server returned a 2xx status. Always inspect resp.StatusCode and close resp.Body after reading it.
Set versus Add
| Method | Behavior | Use it when |
|---|---|---|
Set(name, value) |
Replaces all existing values for the field | There should be one current value, such as an authorization token |
Add(name, value) |
Appends another value | The protocol permits multiple field values or you intentionally send more than one |
req.Header.Set("Accept", "application/json")
req.Header.Add("Accept", "application/problem+json") // sends two values
Header names are case insensitive on the wire. Go canonicalizes names when you use the Header methods, so prefer conventional spellings such as X-Request-ID and Content-Type rather than manipulating map keys directly.
Send headers with a request body
For JSON or form requests, create the body first and set the media type explicitly. The convenience functions such as http.Post do not provide a general way to attach arbitrary fields; use a request and Client.Do.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
payload := map[string]any{"name": "Ada", "active": true}
data, err := json.Marshal(payload)
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.example.com/v1/users", bytes.NewReader(data))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
Do not set transport-controlled fields casually. The HTTP transport determines values such as Content-Length, connection behavior, and some protocol details. Set application headers that the server’s API documents; let the transport manage its own fields.
Reusable helper functions and typed options
Centralizing header construction keeps authentication and tracing consistent across requests. Avoid mutating a shared http.Request concurrently; create a new request for each operation.
type Client struct {
HTTP *http.Client
BaseURL string
Token string
UserAgent string
}
func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, body)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+c.Token)
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
return req, nil
}
For optional values, set only when present. An empty authorization header is usually worse than omitting it because it can hide configuration errors.
Context, timeouts, and cancellation
http.NewRequestWithContext ties DNS lookup, connection setup, redirects, and response reading to a deadline or cancellation signal. Use a client timeout as a safety limit and a context deadline for each operation.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil { return err }
req.Header.Set("X-Correlation-ID", correlationID)
resp, err := client.Do(req)
When a request fails, inspect ctx.Err() to distinguish cancellation or deadline expiry from a connection failure. Do not retry every error: retry only idempotent operations or requests with an idempotency key, and use backoff for transient 5xx and network errors.
Read and validate response headers
Response headers are available through resp.Header. Use Get for the first value and Values when multiple values matter.
contentType := resp.Header.Get("Content-Type")
requestID := resp.Header.Get("X-Request-ID")
setCookies := resp.Header.Values("Set-Cookie")
fmt.Println(contentType, requestID, setCookies)
Validate the status before decoding a successful response. Error responses often use a different content type and schema.
Set custom headers in a Go HTTP server
For server output, call w.Header().Set before w.WriteHeader or the first w.Write. If you omit WriteHeader, the first write commits an implicit 200 response. After that point, ordinary header changes have no effect.
func handler(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = "generated-id"
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Request-ID", requestID)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok":true}`))
}
func main() {
http.HandleFunc("/health", handler)
http.ListenAndServe(":8080", nil)
}
The ResponseWriter documentation states that changing the header map after WriteHeader or Write has no effect, except for 1xx headers and trailers. This timing rule also applies when a helper function writes the body before your code adds a header.
Incoming request headers
Read client-supplied fields from r.Header. Treat them as untrusted input: validate length and allowed characters before using a value in logs, authorization decisions, or another outbound request.
func handler(w http.ResponseWriter, r *http.Request) {
traceID := r.Header.Get("Traceparent")
if len(traceID) > 200 {
http.Error(w, "invalid trace header", http.StatusBadRequest)
return
}
_ = traceID
}
Trailers: values known only after the body
A trailer is different from an ordinary response header because its value is available only after the response body has been generated. If trailer names are known in advance, declare them before writing the response, then assign values later.
func stream(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Trailer", "X-Checksum")
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("streamed data\n"))
w.Header().Set("X-Checksum", "computed-after-stream")
}
Use trailers only when a value truly cannot be known before the response starts. Many clients, proxies, and caches handle ordinary headers more predictably.
Other languages and a screenshot API example
The same header principle applies when an API needs authentication or metadata. ScreenshotNeo is a website screenshot API and MCP server. Its API accepts custom headers, cookies, user agents, authorization, and other capture settings. See the ScreenshotNeo API documentation for the complete option list.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Go
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
endpoint, _ := url.Parse("https://api.screenshotneo.com/v1/shot")
q := endpoint.Query()
q.Set("access_key", "YOUR_API_KEY")
q.Set("url", "https://stripe.com")
endpoint.RawQuery = q.Encode()
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil { panic(err) }
req.Header.Set("Accept", "image/webp")
resp, err := (&http.Client{Timeout: 95 * time.Second}).Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
panic(fmt.Sprintf("screenshot failed: %s: %s", resp.Status, b))
}
f, err := os.Create("shot.webp")
if err != nil { panic(err) }
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil { panic(err) }
fmt.Println("saved shot.webp", resp.Header.Get("X-Page-Verdict"), resp.Header.Get("X-Billed"))
}
Or skip the browser setup
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with the API or MCP server.
Relevant request options and edge cases
- Authentication: use the scheme required by the API, commonly
Authorization: Bearer .... Never log the token. - Content negotiation: send
Acceptfor the response format andContent-Typefor the body you are sending. - Cookies: use
req.AddCookieor theCookieheader for a client request; do not concatenate unvalidated input. - Repeated fields: use
Addonly when the server defines multiple values. Some fields are comma-separated lists; others must be separate lines. - Redirects:
http.Clientfollows redirects by default. Header forwarding across hosts has security implications; configureCheckRedirectwhen needed. - Large bodies: stream with an
io.Readerinstead of loading the entire payload into memory. Limit response sizes withio.LimitReaderwhen the endpoint is untrusted. - Proxies and TLS: custom headers do not replace certificate validation. Keep normal TLS verification enabled unless you have a controlled reason to configure a transport.

Troubleshooting checklist
| Symptom | Cause | Fix |
|---|---|---|
| Header is missing | Used http.Get/http.Post, or wrote the response first |
Create a request with NewRequest and use Client.Do; set server headers before output |
| Old value remains | Called Add when replacement was intended |
Use Set before sending |
| 401 or 403 | Wrong scheme, token, spelling, or target host after redirect | Check the API contract, inspect the final URL and status, and avoid logging secrets |
| Unexpected 200 handling | Do returned nil error but status was an error |
Check the status code before decoding the success schema |
| Context deadline exceeded | Timeout is shorter than DNS, connection, server, or body-read time | Set a realistic context deadline and client timeout; investigate slow upstream work |
| Response header change has no effect | WriteHeader or Write already committed the response |
Move the assignment earlier, or use a declared trailer |
| Duplicate authorization values | Repeated calls to Add |
Use Set for singleton headers and inspect req.Header.Values in tests |
Performance, reliability, and cost considerations
- Reuse clients: keep one configured
http.Clientper transport policy so connections can be reused. Do not create a new client for every request. - Bound work: combine context deadlines with
Client.Timeout; otherwise a stalled server can consume a goroutine indefinitely. - Read efficiently: stream large request and response bodies and close every response body. Connection reuse is reduced when bodies are abandoned.
- Retry carefully: use bounded exponential backoff for transient failures, respect server retry guidance, and avoid replaying non-idempotent requests without an idempotency strategy.
- Observe safely: log method, host, status, duration, and a correlation ID, while redacting authorization, cookies, and other secrets.
- Screenshot cost: with ScreenshotNeo, only clean shots are billed. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; inspect
X-Page-VerdictandX-Billedin your pipeline.
Testing custom headers
Use httptest.NewServer to assert exactly what your client sends without relying on an external service.
func TestHeader(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer test" {
t.Errorf("authorization = %q", got)
}
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil { t.Fatal(err) }
req.Header.Set("Authorization", "Bearer test")
resp, err := http.DefaultClient.Do(req)
if err != nil { t.Fatal(err) }
resp.Body.Close()
}
FAQ
Can I set a header directly on http.Request.Header?
Yes. Prefer Set and Add because they handle canonicalization and make replacement versus append explicit.
Why does http.Get not accept custom headers?
It is a convenience function for simple requests. Build a request with NewRequest, set its headers, and send it with Client.Do.
When should I use a trailer?
Use one only when its value is unavailable until after the body starts streaming. Declare the trailer name before writing the response.
Are header names case sensitive?
HTTP field names are case insensitive. Go’s Header methods canonicalize names, so use conventional spelling for readability.
How can I avoid leaking credentials?
Keep tokens in environment or secret storage, redact authorization and cookies from logs, and avoid putting secrets in URLs because URLs are commonly recorded by proxies and access logs.


