Logo

dev-resources.site

for different kinds of informations.

Golang HTTP Handler With Gzip

Published at
5/26/2022
Categories
go
gzip
compression
beginners
Author
clavinjune
Categories
4 categories in total
go
open
gzip
open
compression
open
beginners
open
Author
10 person written this
clavinjune
open
Golang HTTP Handler With Gzip

Photo by @mildlee on Unsplash

Introduction

Golang has many kinds of compression technique within its standard library which you can use to compress your data. A compression is needed to reduce the size of the data. Even in a web server, a compression technique would be beneficial to increase the communication speed between client and server. Gzip is one of the compression techniques supported by both Golang and web. This article will cover the creation of golang HTTP handler that (de)compress gzip request/response and how to create an HTTP request that send/receive the gzip-compressed body payload. All the code covered in this article you can find it in this repository.

Directory Structure

$ tree .
.
β”œβ”€β”€ LICENSE
β”œβ”€β”€ Makefile
β”œβ”€β”€ README.md
β”œβ”€β”€ client
β”‚Β Β  └── main.go
β”œβ”€β”€ curl.sh
β”œβ”€β”€ go.mod
β”œβ”€β”€ server
β”‚Β Β  └── main.go
└── util.go

2 directories, 8 files
Enter fullscreen mode Exit fullscreen mode

server/main.go

package main

import (
    "example"
    "log"
    "net/http"
)

func main() {
    log.Println("server listening at :8000")
    http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        body := example.MustReadCompressedBody[example.Payload](r.Body)
        body.Number++

        example.MustWriteCompressedResponse(w, body)
    }))
}
Enter fullscreen mode Exit fullscreen mode

The server only run a single endpoint which will read a compressed body, increment the payload content, then respond with new body.

util.go

Payload

You can use the simplest payload just to test the compression, for example:

type Payload struct {
    Number int `json:"number"`
}
Enter fullscreen mode Exit fullscreen mode

Read a Compressed Body Payload

func MustReadCompressedBody[T any](r io.Reader) *T {
    gr, err := gzip.NewReader(r)
    PanicIfErr(err)
    defer gr.Close()

    var t T
    PanicIfErr(json.NewDecoder(gr).Decode(&t))
    return &t
}
Enter fullscreen mode Exit fullscreen mode

To read a gzip-compressed payload, you need to create a gzip.Reader from your response or request. Then decode the JSON as usual using the gzip.Reader instance.

Write a Compressed Response

func MustWriteCompressedResponse(w http.ResponseWriter, body any) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Content-Encoding", "gzip")

    gw := gzip.NewWriter(w)
    defer gw.Close()
    PanicIfErr(json.NewEncoder(gw).Encode(body))
}
Enter fullscreen mode Exit fullscreen mode

As usual, it is better to inform the client about your Content-Type, and Content-Encoding. But in golang case, informing the client about Content-Encoding will help golang http.Request to decompress the payload automatically. Just like reading the payload, you only need to create a gzip.Writer then encode the content normally. Also, don't forget to call gw.Close() to avoid EOF error.

Test the Server

Now, your server has completely (de)compress the request and response. Let's try to test it using cURL command first.

# run the server on another terminal
# using `go run ./server/` command
$ echo '{"number": 99}' | gzip | \
curl -iXPOST http://localhost:8000/ \
-H "Content-Type: application/json" \
-H "Content-Encoding: gzip" \
--compressed --data-binary @-
Enter fullscreen mode Exit fullscreen mode

Since the server will increase the Payload.Number, you can expect the response number will be 100 by sending a request with 99. The expected result would be similar to this:

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json
Date: Thu, 26 May 2022 12:04:53 GMT
Content-Length: 39

{"number":100}
Enter fullscreen mode Exit fullscreen mode

Now, after you sure that your server works completely fine, let's try to send the HTTP request using golang http.Request.

client/main.go

package main

import (
    "context"
    "encoding/json"
    "example"
    "log"
    "net/http"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    body := &example.Payload{
        Number: 100,
    }

    log.Printf("create compressed request with %#v", body)
    req := example.MustCreateCompressedRequest(ctx, http.MethodPost, "http://localhost:8000/", body)
    defer req.Body.Close()

    log.Printf("send compressed request")
    resp, err := http.DefaultClient.Do(req)
    example.PanicIfErr(err)
    defer resp.Body.Close()

    log.Println("resp.Uncompressed?", resp.Uncompressed)
    var responsePayload *example.Payload
    if resp.Uncompressed {
        err = json.NewDecoder(resp.Body).Decode(&responsePayload)
    } else {
        responsePayload = example.MustReadCompressedBody[example.Payload](resp.Body)
    }
    log.Printf("read response %#v", responsePayload)
}
Enter fullscreen mode Exit fullscreen mode

Just like a normal HTTP request, you just need to create an HTTP.Request, send the request, and lastly decode the response. But, before send the request payload, don't forget to compress it first.

Create a Compressed Request

func MustCreateCompressedRequest(ctx context.Context, method, url string, body any) *http.Request {
    pr, pw := io.Pipe()

    go func() {
        gw := gzip.NewWriter(pw)
        err := json.NewEncoder(gw).Encode(body)
        defer PanicIfErr(gw.Close())
        defer pw.CloseWithError(err)
    }()

    r, err := http.NewRequestWithContext(ctx, method, url, pr)
    PanicIfErr(err)

    r.Header.Set("Content-Type", "application/json")
    r.Header.Set("Content-Encoding", "gzip")

    return r
}
Enter fullscreen mode Exit fullscreen mode

Create a new compressed request is quite easy, just like write a compressed response we need to create a new gzip.Writer. Code above utilize io.Pipe to avoid buffer the body into a memory and unecessary allocations. But here's the alternative if you want to buffer the body first:

func MustCreateCompressedRequest(ctx context.Context, method, url string, body any) *http.Request {
    var b bytes.Buffer

    gw := gzip.NewWriter(&b)
    err := json.NewEncoder(gw).Encode(body)
    defer PanicIfErr(gw.Close())

    r, err := http.NewRequestWithContext(ctx, method, url, &b)
    PanicIfErr(err)

    r.Header.Set("Content-Type", "application/json")
    r.Header.Set("Content-Encoding", "gzip")

    return r
}
Enter fullscreen mode Exit fullscreen mode

Again, gw.Close() is also necessary here. If you don't close the gzip.Writer, you'll see an EOF error similar to this:

2022/05/26 19:23:58 http: panic serving [::1]:56436: unexpected EOF
Enter fullscreen mode Exit fullscreen mode

Lastly, after creating the compressed request, you only need to send the request like a normal request. But here's a tricky part.

var responsePayload *example.Payload
if resp.Uncompressed {
    err = json.NewDecoder(resp.Body).Decode(&responsePayload)
} else {
    responsePayload = example.MustReadCompressedBody[example.Payload](resp.Body)
}
Enter fullscreen mode Exit fullscreen mode

After you receive a response which you want to decompress and decode, please be aware with the resp.Uncompressed. If the server returns a header Content-Encoding: gzip, as it said here Golang will try to decompress the payload for you so you don't need to use example.MustReadCompressedBody[example.Payload](resp.Body). But if you add r.Header.Set("Accept-Encoding", "gzip") on your request, it won't be automatically decompressed.

Test the Client

# run the server on another terminal
# using `go run ./server/` command
$ go run ./client
2022/05/26 19:41:34 create compressed request with &example.Payload{Number:100}
2022/05/26 19:41:34 send compressed request
2022/05/26 19:41:34 resp.Uncompressed? true
2022/05/26 19:41:34 read response &example.Payload{Number:101}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementation of the gzip compression on Golang HTTP handler and requests might be adding a little complexity on your code, but I believe modern browser / API gateway these days can implement this easily without changing your code. Also this middleware created by NY Times can help you to minimize the effort on implementing this compression. You can find all the code used on in this article here.

Thank you for reading!

compression Article's
30 articles in total
Favicon
AVIF File Format: The Evolution in Web Image Compression
Favicon
Lightweight, Transparent, Animated: Get All of Them by WebP Format
Favicon
Flutter Image Compression: Ensuring High Quality
Favicon
Compression Is About Knowing Your Data
Favicon
How to use compression in Node.js server for better bandwidth ?
Favicon
Dall.E Image Gen, And Size Comparison Of Image Formats
Favicon
When life gives you lemons ...
Favicon
Image Compression in JavaScript/TypeScript
Favicon
How to Use IMGCentury For Bulk & Unlimited Image Compressions?
Favicon
Created simple phone number compression functions.
Favicon
Exploring the LZ77 Compression Algorithm
Favicon
Ultimate 3D Compression: echo3D Introduces Compression Tools for 3D Content to Accelerate 3D Development
Favicon
Reduce Network Usage - With This 1 Simple Trick!
Favicon
How Finding the Right Compression Level Can Speed Up your Website
Favicon
Client-side image compression with Firebase and Compressor.js
Favicon
Client-side image compression with Supabase Storage
Favicon
COS Cost Optimization Solution
Favicon
Is there any way to compress the data while using mongo persistence with NEventStore?
Favicon
Search safe number compression algorithm(feat. Python)
Favicon
Golang HTTP Handler With Gzip
Favicon
How to use AVIF today!
Favicon
Large documents in redis: is it worth compressing them (Part 1)
Favicon
Open or create TAR files in C# with Aspose.ZIP
Favicon
Supercharge your API with Compression
Favicon
Django 3.2 - News on compressed fixtures and fixturesΒ compression
Favicon
How I made my own file compressor using Node.js
Favicon
Compressed GraalVM Native Images: the best startup for Java apps comes in tiny packages
Favicon
What is lossy and lossless compression
Favicon
did you ever ask yourself how file compression works ? the maths behind greedy algos
Favicon
Huffman coding from scratch with Elixir

Featured ones: