-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from askrella/compression-caching
Add cache and gzip compression
- Loading branch information
Showing
10 changed files
with
304 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
module github.com/askrella/go-mockserver | ||
|
||
go 1.21.1 | ||
go 1.22 | ||
|
||
require github.com/andybalholm/brotli v1.1.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= | ||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package compression | ||
|
||
import ( | ||
"bytes" | ||
gzip2 "compress/gzip" | ||
"github.com/andybalholm/brotli" | ||
"io" | ||
"log" | ||
) | ||
|
||
func DecompressGzip(data []byte) (decompressed []byte) { | ||
reader, err := gzip2.NewReader(bytes.NewBuffer(data)) | ||
if err != nil { | ||
log.Panicln("Cannot create new gzip reader:", err) | ||
} | ||
defer func(reader *gzip2.Reader) { | ||
err := reader.Close() | ||
if err != nil { | ||
log.Panicln("Error closing gzip reader: ", err) | ||
} | ||
}(reader) | ||
|
||
decompressed, err = io.ReadAll(reader) | ||
if err != nil { | ||
log.Panicln("Reading decompressed data from gzip reader caused an error: ", err) | ||
} | ||
|
||
return decompressed | ||
} | ||
|
||
func CompressBrotli(data []byte) (compressed []byte, contentEncoding string) { | ||
out := bytes.Buffer{} | ||
writer := brotli.NewWriterV2(&out, brotli.BestSpeed) | ||
_, err := writer.Write(data) | ||
if err != nil { | ||
log.Panicln("Error writing to brotli writer: ", err) | ||
} | ||
|
||
err = writer.Close() | ||
if err != nil { | ||
log.Panicln("Error closing brotli writer: ", err) | ||
} | ||
|
||
return out.Bytes(), "br" | ||
} | ||
|
||
func CompressGzip(data []byte) (compressed []byte, contentEncoding string) { | ||
var b bytes.Buffer | ||
gzw, err := gzip2.NewWriterLevel(&b, 9) | ||
if err != nil { | ||
log.Panicln("Error creating gzip writer:", err) | ||
} | ||
_, err = gzw.Write(data) | ||
if err != nil { | ||
log.Panicln("gzip write error:", err) | ||
} | ||
err = gzw.Flush() | ||
if err != nil { | ||
log.Panicln("gzip flush error:", err) | ||
} | ||
err = gzw.Close() | ||
if err != nil { | ||
log.Panicln("gzip close error:", err) | ||
} | ||
|
||
return b.Bytes(), "gzip" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package server | ||
|
||
import ( | ||
"bytes" | ||
"github.com/askrella/go-mockserver/internal/compression" | ||
"github.com/askrella/go-mockserver/internal/config" | ||
"github.com/askrella/go-mockserver/internal/store" | ||
"io" | ||
"log" | ||
"net/http" | ||
"strconv" | ||
) | ||
|
||
// CacheCompressionHandler provides the ability to | ||
// 1. Store and replay requests and | ||
// 2. DecompressGzip upstream and compress with stronger algorithms/levels for downstream | ||
func CacheCompressionHandler(handler http.Handler) http.Handler { | ||
return http.HandlerFunc(func(respWriter http.ResponseWriter, request *http.Request) { | ||
if config.CacheEnabled() { | ||
if cacheUsed := useTransactionResponseCache(respWriter, request); cacheUsed { | ||
return | ||
} | ||
} | ||
|
||
var resultingBody []byte | ||
|
||
// Intercept the response request with a custom request and get the body data. | ||
var buffer bytes.Buffer | ||
decompressedResponseWriter := &responseWriterInterceptor{ResponseWriter: respWriter, Writer: &buffer} | ||
handler.ServeHTTP(decompressedResponseWriter, request) | ||
body := buffer.Bytes() | ||
resultingBody = body | ||
|
||
// Check if the content encoding is gzip and in case it is not, skip | ||
currentEncoding := decompressedResponseWriter.Header().Get("Content-Encoding") | ||
if currentEncoding != "gzip" { | ||
log.Println("Skip compression, content-encoding: " + currentEncoding) | ||
_, err := respWriter.Write(body) | ||
if err != nil { | ||
respWriter.WriteHeader(http.StatusInternalServerError) | ||
log.Println("Error writing original response without compression: ", err) | ||
return | ||
} | ||
|
||
return | ||
} | ||
|
||
if config.Recompress() { | ||
// DecompressGzip and re-compress. This seems weird at first, but we want to control how the content | ||
// gets compressed and sent to the downstream. For example: when loading a huge CSV dataset, we saw substantial | ||
// performance improvements by using level 9 gzip instead of the original gzip compression. | ||
decompressed := compression.DecompressGzip(body) | ||
compressed, contentEncoding := compression.CompressGzip(decompressed) | ||
contentLength := strconv.FormatInt(int64(len(compressed)), 10) | ||
|
||
respWriter.Header().Set("Content-Encoding", contentEncoding) | ||
respWriter.Header().Set("Content-Length", contentLength) | ||
_, err := respWriter.Write(compressed) | ||
if err != nil { | ||
respWriter.WriteHeader(http.StatusInternalServerError) | ||
log.Println("Failed to write response", err) | ||
return | ||
} | ||
|
||
resultingBody = compressed | ||
} | ||
|
||
if config.CacheEnabled() { | ||
storeTransactionResponse(respWriter, request, resultingBody) | ||
} | ||
}) | ||
} | ||
|
||
func useTransactionResponseCache(respWriter http.ResponseWriter, request *http.Request) (cacheUsed bool) { | ||
transaction, found := store.FindTransaction(request) | ||
// Cache handling: If a transaction is found, use the body from the cached transaction. | ||
if !found { | ||
return false | ||
} | ||
log.Println("Found transaction, using cache.") | ||
response := transaction.Response | ||
|
||
// Write headers from cached transaction to downstream | ||
for key, value := range response.Headers { | ||
for _, headerValue := range value { | ||
respWriter.Header().Add(key, headerValue) | ||
} | ||
} | ||
|
||
_, err := respWriter.Write(response.Body) | ||
if err != nil { | ||
log.Println(err) | ||
respWriter.WriteHeader(http.StatusInternalServerError) | ||
return true | ||
} | ||
|
||
return true | ||
} | ||
|
||
func storeTransactionResponse(respWriter http.ResponseWriter, request *http.Request, compressed []byte) { | ||
// Find transaction and store our compressed content | ||
transaction, found := store.FindTransaction(request) | ||
if !found { | ||
respWriter.WriteHeader(http.StatusInternalServerError) | ||
log.Panicln("Transaction not found") | ||
return | ||
} | ||
|
||
transaction.Response = store.Response{ | ||
Body: compressed, | ||
Headers: respWriter.Header(), | ||
} | ||
} | ||
|
||
type responseWriterInterceptor struct { | ||
http.ResponseWriter | ||
Body io.Reader | ||
Writer io.Writer | ||
} | ||
|
||
func (w *responseWriterInterceptor) Write(b []byte) (int, error) { | ||
return w.Writer.Write(b) | ||
} | ||
|
||
func (w *responseWriterInterceptor) WriteHeader(statusCode int) { | ||
w.ResponseWriter.WriteHeader(statusCode) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.