Skip to content

Commit

Permalink
Issue #14: added support for response body compression
Browse files Browse the repository at this point in the history
  • Loading branch information
valyala committed Dec 25, 2015
1 parent 84e14eb commit edcfdbc
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 0 deletions.
138 changes: 138 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package fasthttp
import (
"bufio"
"bytes"
"compress/flate"
"compress/gzip"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"os"
"sync"
Expand Down Expand Up @@ -195,6 +198,45 @@ func (resp *Response) Body() []byte {
return resp.body
}

// BodyGunzip returns un-gzipped body data.
//
// This method may be used if the response header contains
// 'Content-Encoding: gzip' for reading un-gzipped response body.
// Use Body for reading gzipped response body.
func (resp *Response) BodyGunzip() ([]byte, error) {
// Do not care about memory allocations here,
// since gzip is slow and generates a lot of memory allocations
// by itself.
r, err := gzip.NewReader(bytes.NewBuffer(resp.body))
if err != nil {
return nil, err
}
defer r.Close()
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
return b, nil
}

// BodyInflate returns un-deflated body data.
//
// This method may be used if the response header contains
// 'Content-Encoding: deflate' for reading un-deflated response body.
// Use Body for reading deflated response body.
func (resp *Response) BodyInflate() ([]byte, error) {
// Do not care about memory allocations here,
// since flate is slow and generates a lot of memory allocations
// by itself.
r := flate.NewReader(bytes.NewBuffer(resp.body))
defer r.Close()
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
return b, nil
}

// AppendBody appends p to response body.
func (resp *Response) AppendBody(p []byte) {
resp.closeBodyStream()
Expand Down Expand Up @@ -520,6 +562,102 @@ func (req *Request) Write(w *bufio.Writer) error {
return err
}

// WriteGzip writes response with gzipped body to w.
//
// The method sets 'Content-Encoding: gzip' header.
//
// WriteGzip doesn't flush response to w for performance reasons.
func (resp *Response) WriteGzip(w *bufio.Writer) error {
return resp.WriteGzipLevel(w, gzip.DefaultCompression)
}

// WriteGzipLevel writes response with gzipped body to w.
//
// Level is compression level. See available levels in encoding/gzip package.
//
// The method sets 'Content-Encoding: gzip' header.
//
// WriteGzipLevel doesn't flush response to w for performance reasons.
func (resp *Response) WriteGzipLevel(w *bufio.Writer, level int) error {
// Do not care about memory allocations here, since gzip is slow
// and allocates a lot of memory by itself.
if resp.bodyStream != nil {
bs := resp.bodyStream
resp.bodyStream = NewStreamReader(func(sw *bufio.Writer) {
zw := newGzipWriter(sw, level)
defer zw.Close()
io.Copy(zw, bs)
})
} else {
var buf bytes.Buffer
zw := newGzipWriter(&buf, level)
if _, err := zw.Write(resp.body); err != nil {
return err
}
zw.Close()
resp.body = buf.Bytes()
}

resp.Header.SetCanonical(strContentEncoding, strGzip)
return resp.Write(w)
}

// WriteDeflate writes response with deflated body to w.
//
// The method sets 'Content-Encoding: deflate' header.
//
// WriteDeflate doesn't flush response to w for performance reasons.
func (resp *Response) WriteDeflate(w *bufio.Writer) error {
return resp.WriteDeflateLevel(w, flate.DefaultCompression)
}

// WriteDeflateLevel writes response with deflated body to w.
//
// Level is compression level. See available levels in encoding/flate package.
//
// The method sets 'Content-Encoding: deflate' header.
//
// WriteDeflateLevel doesn't flush response to w for performance reasons.
func (resp *Response) WriteDeflateLevel(w *bufio.Writer, level int) error {
// Do not care about memory allocations here, since flate is slow
// and allocates a lot of memory by itself.
if resp.bodyStream != nil {
bs := resp.bodyStream
resp.bodyStream = NewStreamReader(func(sw *bufio.Writer) {
zw := newDeflateWriter(sw, level)
defer zw.Close()
io.Copy(zw, bs)
})
} else {
var buf bytes.Buffer
zw := newDeflateWriter(&buf, level)
if _, err := zw.Write(resp.body); err != nil {
return err
}
zw.Close()
resp.body = buf.Bytes()
}

resp.Header.SetCanonical(strContentEncoding, strDeflate)
return resp.Write(w)
}

func newDeflateWriter(w io.Writer, level int) *flate.Writer {
zw, err := flate.NewWriter(w, level)
if err != nil {
panic(fmt.Sprintf("BUG: flate.NewWriter(%d) returns non-nil error: %s", level, err))
}
return zw
}

func newGzipWriter(w io.Writer, level int) *gzip.Writer {
zw, err := gzip.NewWriterLevel(w, level)
if err != nil {
panic(fmt.Sprintf("BUG: gzip.NewWriter(%d) returns non-nil error: %s", level, err))
}
return zw
}

// Write writes response to w.
//
// Write doesn't flush response to w for performance reasons.
Expand Down
110 changes: 110 additions & 0 deletions http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,116 @@ import (
"testing"
)

func TestResponseGzipStream(t *testing.T) {
var r Response
r.SetBodyStreamWriter(func(w *bufio.Writer) {
fmt.Fprintf(w, "foo")
w.Flush()
w.Write([]byte("barbaz"))
w.Flush()
fmt.Fprintf(w, "1234")
if err := w.Flush(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
})
testResponseGzipExt(t, &r, "foobarbaz1234")
}

func TestResponseDeflateStream(t *testing.T) {
var r Response
r.SetBodyStreamWriter(func(w *bufio.Writer) {
w.Write([]byte("foo"))
w.Flush()
fmt.Fprintf(w, "barbaz")
w.Flush()
w.Write([]byte("1234"))
if err := w.Flush(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
})
testResponseDeflateExt(t, &r, "foobarbaz1234")
}

func TestResponseDeflate(t *testing.T) {
testResponseDeflate(t, "")
testResponseDeflate(t, "abdasdfsdaa")
testResponseDeflate(t, "asoiowqoieroqweiruqwoierqo")
}

func TestResponseGzip(t *testing.T) {
testResponseGzip(t, "")
testResponseGzip(t, "foobarbaz")
testResponseGzip(t, "abasdwqpweoweporweprowepr")
}

func testResponseDeflate(t *testing.T, s string) {
var r Response
r.SetBodyString(s)
testResponseDeflateExt(t, &r, s)
}

func testResponseDeflateExt(t *testing.T, r *Response, s string) {
var buf bytes.Buffer
bw := bufio.NewWriter(&buf)
if err := r.WriteDeflate(bw); err != nil {
t.Fatalf("unexpected error: %s", err)
}
if err := bw.Flush(); err != nil {
t.Fatalf("unexpected error: %s", err)
}

var r1 Response
br := bufio.NewReader(&buf)
if err := r1.Read(br); err != nil {
t.Fatalf("unexpected error: %s", err)
}
ce := r1.Header.Peek("Content-Encoding")
if string(ce) != "deflate" {
t.Fatalf("unexpected Content-Encoding %q. Expecting %q", ce, "deflate")
}
body, err := r1.BodyInflate()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if string(body) != s {
t.Fatalf("unexpected body %q. Expecting %q", body, s)
}
}

func testResponseGzip(t *testing.T, s string) {
var r Response
r.SetBodyString(s)
testResponseGzipExt(t, &r, s)
}

func testResponseGzipExt(t *testing.T, r *Response, s string) {
var buf bytes.Buffer
bw := bufio.NewWriter(&buf)
if err := r.WriteGzip(bw); err != nil {
t.Fatalf("unexpected error: %s", err)
}
if err := bw.Flush(); err != nil {
t.Fatalf("unexpected error: %s", err)
}

var r1 Response
br := bufio.NewReader(&buf)
if err := r1.Read(br); err != nil {
t.Fatalf("unexpected error: %s", err)
}
ce := r1.Header.Peek("Content-Encoding")
if string(ce) != "gzip" {
t.Fatalf("unexpected Content-Encoding %q. Expecting %q", ce, "gzip")
}
body, err := r1.BodyGunzip()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if string(body) != s {
t.Fatalf("unexpected body %q. Expecting %q", body, s)
}
}

func TestRequestMultipartForm(t *testing.T) {
var w bytes.Buffer
mw := multipart.NewWriter(&w)
Expand Down
3 changes: 3 additions & 0 deletions strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
strReferer = []byte("Referer")
strServer = []byte("Server")
strTransferEncoding = []byte("Transfer-Encoding")
strContentEncoding = []byte("Content-Encoding")
strUserAgent = []byte("User-Agent")
strCookie = []byte("Cookie")
strSetCookie = []byte("Set-Cookie")
Expand All @@ -44,6 +45,8 @@ var (
strCookiePath = []byte("path")

strClose = []byte("close")
strGzip = []byte("gzip")
strDeflate = []byte("deflate")
strKeepAlive = []byte("keep-alive")
strKeepAliveCamelCase = []byte("Keep-Alive")
strUpgrade = []byte("Upgrade")
Expand Down

0 comments on commit edcfdbc

Please sign in to comment.