Skip to content

Commit

Permalink
feat: support gzip compression
Browse files Browse the repository at this point in the history
  • Loading branch information
gernest committed Feb 11, 2024
1 parent be532d3 commit dd169ea
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 5 deletions.
69 changes: 64 additions & 5 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package api

import (
"compress/gzip"
"context"
"crypto/subtle"
"io"
"net/http"
"strings"
"sync"

"github.com/vinceanalytics/vince/buffers"
v1 "github.com/vinceanalytics/vince/gen/go/staples/v1"
"github.com/vinceanalytics/vince/guard"
"github.com/vinceanalytics/vince/logger"
Expand All @@ -16,19 +20,51 @@ import (
"github.com/vinceanalytics/vince/version"
)

const (
vary = "Vary"
acceptEncoding = "Accept-Encoding"
contentEncoding = "Content-Encoding"
contentType = "Content-Type"
contentLength = "Content-Length"
)

type API struct {
config *v1.Config
hand http.Handler
}

var trackerServer = http.FileServer(http.FS(tracker.JS))

var gzipPool = &sync.Pool{New: func() any {
// To scale , we know the payload is JSON and the number of calls+data
// introduces large egress cost.
//
// Optimize for size
w, err := gzip.NewWriterLevel(nil, gzip.BestCompression)
if err != nil {
logger.Fail("Failed creating gzip writer", "err", err)
}
return w
}}

const minSizeToCompress = 1 << 10

func getZip() *gzip.Writer {
return gzipPool.Get().(*gzip.Writer)
}

func putZip(w *gzip.Writer) {
w.Reset(io.Discard)
gzipPool.Put(w)
}

func New(ctx context.Context, o *v1.Config) (*API, error) {
a := &API{
config: o,
}
base := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code := &responseCode{ResponseWriter: w}
w.Header().Add(vary, acceptEncoding)
code := &statsWriter{ResponseWriter: w, compress: acceptsGzip(r)}
defer func() {
logger.Get(r.Context()).Debug(r.URL.String(), "method", r.Method, "status", code.code)
}()
Expand Down Expand Up @@ -89,14 +125,37 @@ func New(ctx context.Context, o *v1.Config) (*API, error) {
return a, nil
}

type responseCode struct {
type statsWriter struct {
http.ResponseWriter
code int
raw int
compressed int
compress bool
code int
}

func (r *statsWriter) Write(p []byte) (int, error) {
// All writes to response are a single call.
r.raw = len(p)
if !r.compress || len(p) <= minSizeToCompress {
return r.ResponseWriter.Write(p)
}
r.Header().Set(contentEncoding, "gzip")
r.Header().Del(contentLength)
if r.code != 0 {
r.ResponseWriter.WriteHeader(r.code)
}
b := buffers.Bytes()
defer b.Release()
g := getZip()
defer putZip(g)
g.Reset(b)
g.Write(p)
r.compressed = b.Len()
return r.ResponseWriter.Write(b.Bytes())
}

func (r *responseCode) WriteHeader(code int) {
func (r *statsWriter) WriteHeader(code int) {
r.code = code
r.ResponseWriter.WriteHeader(code)
}

func parseBearer(auth string) (token string) {
Expand Down
91 changes: 91 additions & 0 deletions api/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Code in this file is taken from https://github.com/nytimes/gziphandler.git
// which is licensed under Apache 2.0

package api

import (
"fmt"
"net/http"
"strconv"
"strings"
)

type codings map[string]float64

const (
// DefaultQValue is the default qvalue to assign to an encoding if no explicit qvalue is set.
// This is actually kind of ambiguous in RFC 2616, so hopefully it's correct.
// The examples seem to indicate that it is.
DefaultQValue = 1.0

// DefaultMinSize is the default minimum size until we enable gzip compression.
// 1500 bytes is the MTU size for the internet since that is the largest size allowed at the network layer.
// If you take a file that is 1300 bytes and compress it to 800 bytes, it’s still transmitted in that same 1500 byte packet regardless, so you’ve gained nothing.
// That being the case, you should restrict the gzip compression to files with a size greater than a single packet, 1400 bytes (1.4KB) is a safe value.
DefaultMinSize = 1400
)

// acceptsGzip returns true if the given HTTP request indicates that it will
// accept a gzipped response.
func acceptsGzip(r *http.Request) bool {
acceptedEncodings, _ := parseEncodings(r.Header.Get(acceptEncoding))
return acceptedEncodings["gzip"] > 0.0
}

// parseEncodings attempts to parse a list of codings, per RFC 2616, as might
// appear in an Accept-Encoding header. It returns a map of content-codings to
// quality values, and an error containing the errors encountered. It's probably
// safe to ignore those, because silently ignoring errors is how the internet
// works.
//
// See: http://tools.ietf.org/html/rfc2616#section-14.3.
func parseEncodings(s string) (codings, error) {
c := make(codings)
var e []string

for _, ss := range strings.Split(s, ",") {
coding, qvalue, err := parseCoding(ss)

if err != nil {
e = append(e, err.Error())
} else {
c[coding] = qvalue
}
}

// TODO (adammck): Use a proper multi-error struct, so the individual errors
// can be extracted if anyone cares.
if len(e) > 0 {
return c, fmt.Errorf("errors while parsing encodings: %s", strings.Join(e, ", "))
}

return c, nil
}

// parseCoding parses a single conding (content-coding with an optional qvalue),
// as might appear in an Accept-Encoding header. It attempts to forgive minor
// formatting errors.
func parseCoding(s string) (coding string, qvalue float64, err error) {
for n, part := range strings.Split(s, ";") {
part = strings.TrimSpace(part)
qvalue = DefaultQValue

if n == 0 {
coding = strings.ToLower(part)
} else if strings.HasPrefix(part, "q=") {
qvalue, err = strconv.ParseFloat(strings.TrimPrefix(part, "q="), 64)

if qvalue < 0.0 {
qvalue = 0.0
} else if qvalue > 1.0 {
qvalue = 1.0
}
}
}

if coding == "" {
err = fmt.Errorf("empty content-coding")
}

return
}

0 comments on commit dd169ea

Please sign in to comment.