refactor(response): use http.Header for header map

http.Header is built for headers, so use it for its intended purpose
This commit is contained in:
gudvinr
2026-05-28 23:33:25 +03:00
committed by fguillot
parent 79381e6f9a
commit 9a774083ab
+17 -18
View File
@@ -8,6 +8,7 @@ import (
"compress/gzip"
"io"
"log/slog"
"maps"
"mime"
"net/http"
"strings"
@@ -23,14 +24,14 @@ type Builder struct {
w http.ResponseWriter
r *http.Request
statusCode int
headers map[string]string
headers http.Header
enableCompression bool
body any
}
// NewBuilder creates a new response builder.
func NewBuilder(w http.ResponseWriter, r *http.Request) *Builder {
return &Builder{w: w, r: r, statusCode: http.StatusOK, headers: make(map[string]string), enableCompression: true}
return &Builder{w: w, r: r, statusCode: http.StatusOK, headers: make(http.Header), enableCompression: true}
}
// WithStatus uses the given status code to build the response.
@@ -41,7 +42,7 @@ func (b *Builder) WithStatus(statusCode int) *Builder {
// WithHeader adds the given HTTP header to the response.
func (b *Builder) WithHeader(key, value string) *Builder {
b.headers[key] = value
b.headers.Set(key, value)
return b
}
@@ -65,13 +66,13 @@ func (b *Builder) WithBodyAsReader(body io.Reader) *Builder {
// WithAttachment forces the document to be downloaded by the web browser.
func (b *Builder) WithAttachment(filename string) *Builder {
b.headers["Content-Disposition"] = formatContentDisposition("attachment", filename)
b.headers.Set("Content-Disposition", formatContentDisposition("attachment", filename))
return b
}
// WithInline suggests an inline filename for the current response.
func (b *Builder) WithInline(filename string) *Builder {
b.headers["Content-Disposition"] = formatContentDisposition("inline", filename)
b.headers.Set("Content-Disposition", formatContentDisposition("inline", filename))
return b
}
@@ -84,9 +85,9 @@ func (b *Builder) WithoutCompression() *Builder {
// WithCaching adds caching headers to the response.
func (b *Builder) WithCaching(etag string, duration time.Duration, callback func(*Builder)) {
etag = normalizeETag(etag)
b.headers["ETag"] = etag
b.headers["Cache-Control"] = "public, immutable"
b.headers["Expires"] = time.Now().Add(duration).UTC().Format(http.TimeFormat)
b.headers.Set("ETag", etag)
b.headers.Set("Cache-Control", "public, immutable")
b.headers.Set("Expires", time.Now().Add(duration).UTC().Format(http.TimeFormat))
if ifNoneMatch(b.r.Header.Get("If-None-Match"), etag) {
b.statusCode = http.StatusNotModified
@@ -120,24 +121,22 @@ func (b *Builder) Write() {
}
func (b *Builder) writeHeaders() {
b.headers["X-Content-Type-Options"] = "nosniff"
b.headers["X-Frame-Options"] = "DENY"
b.headers["Referrer-Policy"] = "no-referrer"
b.headers.Set("X-Content-Type-Options", "nosniff")
b.headers.Set("X-Frame-Options", "DENY")
b.headers.Set("Referrer-Policy", "no-referrer")
for key, value := range b.headers {
b.w.Header().Set(key, value)
}
maps.Copy(b.w.Header(), b.headers)
b.w.WriteHeader(b.statusCode)
}
func (b *Builder) compress(data []byte) {
if b.enableCompression && len(data) > compressionThreshold {
b.headers["Vary"] = "Accept-Encoding"
b.headers.Set("Vary", "Accept-Encoding")
acceptEncoding := b.r.Header.Get("Accept-Encoding")
switch {
case strings.Contains(acceptEncoding, "br"):
b.headers["Content-Encoding"] = "br"
b.headers.Set("Content-Encoding", "br")
b.writeHeaders()
brotliWriter := brotli.NewWriterV2(b.w, brotli.DefaultCompression)
@@ -145,7 +144,7 @@ func (b *Builder) compress(data []byte) {
brotliWriter.Close()
return
case strings.Contains(acceptEncoding, "gzip"):
b.headers["Content-Encoding"] = "gzip"
b.headers.Set("Content-Encoding", "gzip")
b.writeHeaders()
gzipWriter := gzip.NewWriter(b.w)
@@ -153,7 +152,7 @@ func (b *Builder) compress(data []byte) {
gzipWriter.Close()
return
case strings.Contains(acceptEncoding, "deflate"):
b.headers["Content-Encoding"] = "deflate"
b.headers.Set("Content-Encoding", "deflate")
b.writeHeaders()
flateWriter, _ := flate.NewWriter(b.w, -1)