|
|
@@ -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)
|