Skip to content

Commit

Permalink
per request gzip disable option, gzip streamline
Browse files Browse the repository at this point in the history
  • Loading branch information
jeevatkm committed May 7, 2017
1 parent 636d4cc commit e7b04f2
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 55 deletions.
60 changes: 29 additions & 31 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ type (
isRequestIDEnabled bool
requestIDHeader string
isGzipEnabled bool
gzipLevel int
ctxPool *pool.Pool
reqPool *pool.Pool
replyPool *pool.Pool
Expand All @@ -63,9 +62,7 @@ func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

ctx := e.prepareContext(w, r)
defer func() {
e.putContext(ctx)
}()
defer e.putContext(ctx)

// Recovery handling, capture every possible panic(s)
defer e.handleRecovery(ctx)
Expand Down Expand Up @@ -134,17 +131,11 @@ func (e *engine) setRequestID(r *http.Request) {
// controller, parses the request and returns the controller.
func (e *engine) prepareContext(w http.ResponseWriter, req *http.Request) *Context {
ctx, r := e.getContext(), e.getRequest()

ctx.Req = ahttp.ParseRequest(req, r)
ctx.Res = ahttp.GetResponseWriter(w)
ctx.reply = e.getReply()
ctx.viewArgs = make(map[string]interface{})

if ctx.Req.IsGzipAccepted && e.isGzipEnabled {
ctx.Res = ahttp.WrapGzipResponseWriter(w, e.gzipLevel)
} else {
ctx.Res = ahttp.WrapResponseWriter(w)
}

return ctx
}

Expand Down Expand Up @@ -272,7 +263,13 @@ func (e *engine) writeReply(ctx *Context) {
}

// Gzip
e.writeGzipHeaders(ctx, buf.Len() == 0)
if ctx.Req.IsGzipAccepted && e.isGzipEnabled && reply.gzip {
if reply.Code != http.StatusNoContent &&
reply.Code != http.StatusNotModified &&
buf.Len() != 0 {
e.wrapGzipWriter(ctx)
}
}

// HTTP headers
e.writeHeaders(ctx)
Expand All @@ -290,6 +287,15 @@ func (e *engine) writeReply(ctx *Context) {
publishOnAfterReplyEvent(ctx)
}

// wrapGzipWriter method writes respective header for gzip and wraps write into
// gzip writer.
func (e *engine) wrapGzipWriter(ctx *Context) {
ctx.Res.Header().Add(ahttp.HeaderVary, ahttp.HeaderAcceptEncoding)
ctx.Res.Header().Add(ahttp.HeaderContentEncoding, gzipContentEncoding)
ctx.Res.Header().Del(ahttp.HeaderContentLength)
ctx.Res = ahttp.GetGzipResponseWriter(ctx.Res)
}

// writeHeaders method writes the headers on the wire.
func (e *engine) writeHeaders(ctx *Context) {
for k, v := range ctx.Reply().Hdr {
Expand All @@ -312,19 +318,6 @@ func (e *engine) writeHeaders(ctx *Context) {
}
}

// writeGzipHeaders method prepares appropriate headers for gzip response
func (e *engine) writeGzipHeaders(ctx *Context, delCntEnc bool) {
if ctx.Req.IsGzipAccepted && e.isGzipEnabled {
ctx.Res.Header().Add(ahttp.HeaderVary, ahttp.HeaderAcceptEncoding)
ctx.Res.Header().Add(ahttp.HeaderContentEncoding, gzipContentEncoding)
ctx.Res.Header().Del(ahttp.HeaderContentLength)

if ctx.Reply().Code == http.StatusNoContent || delCntEnc {
ctx.Res.Header().Del(ahttp.HeaderContentEncoding)
}
}
}

// setCookies method sets the user cookies, session cookie and saves session
// into session store is session mode is stateful.
func (e *engine) setCookies(ctx *Context) {
Expand Down Expand Up @@ -356,8 +349,14 @@ func (e *engine) getReply() *Reply {

// putContext method puts context back to pool
func (e *engine) putContext(ctx *Context) {
// Try to close if `io.Closer` interface satisfies.
ess.CloseQuietly(ctx.Res)
// Close the writer and Put back to pool
if ctx.Res != nil {
if _, ok := ctx.Res.(*ahttp.GzipResponse); ok {
ahttp.PutGzipResponseWiriter(ctx.Res)
} else {
ahttp.PutResponseWriter(ctx.Res)
}
}

// clear and put `ahttp.Request` into pool
if ctx.Req != nil {
Expand Down Expand Up @@ -392,16 +391,15 @@ func (e *engine) putBuffer(b *bytes.Buffer) {
//___________________________________

func newEngine(cfg *config.Config) *engine {
gzipLevel := cfg.IntDefault("render.gzip.level", 1)
if !(gzipLevel >= 1 && gzipLevel <= 9) {
logAsFatal(fmt.Errorf("'render.gzip.level' is not a valid level value: %v", gzipLevel))
ahttp.GzipLevel = cfg.IntDefault("render.gzip.level", 5)
if !(ahttp.GzipLevel >= 1 && ahttp.GzipLevel <= 9) {
logAsFatal(fmt.Errorf("'render.gzip.level' is not a valid level value: %v", ahttp.GzipLevel))
}

return &engine{
isRequestIDEnabled: cfg.BoolDefault("request.id.enable", true),
requestIDHeader: cfg.StringDefault("request.id.header", ahttp.HeaderXRequestID),
isGzipEnabled: cfg.BoolDefault("render.gzip.enable", true),
gzipLevel: gzipLevel,
ctxPool: pool.NewPool(
cfg.IntDefault("runtime.pooling.global", 0),
func() interface{} {
Expand Down
42 changes: 33 additions & 9 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package aah

import (
"compress/gzip"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand All @@ -26,9 +27,19 @@ type (
)

func (s *Site) GetInvolved() {
s.Session().Set("test1", "test1value")
s.Reply().Text("GetInvolved action")
}

func (s *Site) Credits() {
s.Reply().Header("X-Custom-Header", "custom value").
DisableGzip().
JSON(map[string]interface{}{
"message": "This is credits page",
"code": 1000001,
})
}

func (s *Site) ContributeCode() {
panic("panic flow testing")
}
Expand Down Expand Up @@ -64,7 +75,6 @@ func TestEngineNew(t *testing.T) {

e := newEngine(AppConfig())
assert.Equal(t, "X-Test-Request-Id", e.requestIDHeader)
assert.Equal(t, 5, e.gzipLevel)
assert.True(t, e.isRequestIDEnabled)
assert.True(t, e.isGzipEnabled)
assert.NotNil(t, e.ctxPool)
Expand Down Expand Up @@ -101,6 +111,7 @@ func TestEngineServeHTTP(t *testing.T) {
// Security
err = initSecurity(cfgDir, AppConfig())
assert.Nil(t, err)
assert.True(t, AppSessionManager().IsStateful())

// Controllers
cRegistry = controllerRegistry{}
Expand All @@ -114,6 +125,10 @@ func TestEngineServeHTTP(t *testing.T) {
Name: "ContributeCode",
Parameters: []*ParameterInfo{},
},
{
Name: "Credits",
Parameters: []*ParameterInfo{},
},
})

// Middlewares
Expand All @@ -134,11 +149,13 @@ func TestEngineServeHTTP(t *testing.T) {

// Request 2
r2 := httptest.NewRequest("GET", "http://localhost:8080/get-involved.html", nil)
r2.Header.Add(ahttp.HeaderAcceptEncoding, "gzip, deflate, sdch, br")
w2 := httptest.NewRecorder()
e.ServeHTTP(w2, r2)

resp2 := w2.Result()
body2, _ := ioutil.ReadAll(resp2.Body)
gr2, _ := gzip.NewReader(resp2.Body)
body2, _ := ioutil.ReadAll(gr2)
assert.Equal(t, 200, resp2.StatusCode)
assert.Equal(t, "OK", resp2.Status)
assert.Equal(t, "GetInvolved action", string(body2))
Expand Down Expand Up @@ -177,6 +194,8 @@ func TestEngineServeHTTP(t *testing.T) {
assert.Equal(t, "http://localhost:8080/testdata/", resp5.Header.Get(ahttp.HeaderLocation))

r6 := httptest.NewRequest("GET", "http://localhost:8080/testdata/", nil)
r6.Header.Add(e.requestIDHeader, "D9391509-595B-4B92-BED7-F6A9BE0DFCF2")
r6.Header.Add(ahttp.HeaderAcceptEncoding, "gzip, deflate, sdch, br")
w6 := httptest.NewRecorder()
e.ServeHTTP(w6, r6)

Expand All @@ -185,6 +204,17 @@ func TestEngineServeHTTP(t *testing.T) {
body6Str := string(body6)
assert.True(t, strings.Contains(body6Str, "Listing of /testdata/"))
assert.True(t, strings.Contains(body6Str, "config/"))

r7 := httptest.NewRequest("GET", "http://localhost:8080/credits", nil)
r7.Header.Add(ahttp.HeaderAcceptEncoding, "gzip, deflate, sdch, br")
w7 := httptest.NewRecorder()
e.ServeHTTP(w7, r7)

resp7 := w7.Result()
body7, _ := ioutil.ReadAll(resp7.Body)
body7Str := string(body7)
assert.Equal(t, `{"code":1000001,"message":"This is credits page"}`, body7Str)
assert.Equal(t, "custom value", resp7.Header.Get("X-Custom-Header"))
appBaseDir = ""
}

Expand All @@ -195,15 +225,9 @@ func TestEngineGzipHeaders(t *testing.T) {
req := httptest.NewRequest("GET", "http://localhost:8080/doc/v0.3/mydoc.html", nil)
req.Header.Add(ahttp.HeaderAcceptEncoding, "gzip")
ctx := e.prepareContext(httptest.NewRecorder(), req)
e.wrapGzipWriter(ctx)

assert.True(t, ctx.Req.IsGzipAccepted)

e.writeGzipHeaders(ctx, false)

assert.Equal(t, "gzip", ctx.Res.Header().Get(ahttp.HeaderContentEncoding))
assert.Equal(t, "Accept-Encoding", ctx.Res.Header().Get(ahttp.HeaderVary))

e.writeGzipHeaders(ctx, true)
assert.Equal(t, "", ctx.Res.Header().Get(ahttp.HeaderContentEncoding))
assert.Equal(t, "Accept-Encoding", ctx.Res.Header().Get(ahttp.HeaderVary))
}
2 changes: 1 addition & 1 deletion middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestMiddlewareToHandler(t *testing.T) {
req := httptest.NewRequest("GET", "http://localhost:8080/doc/v0.3/mydoc.html", nil)
ctx := &Context{
Req: ahttp.ParseRequest(req, &ahttp.Request{}),
Res: ahttp.WrapResponseWriter(httptest.NewRecorder()),
Res: ahttp.GetResponseWriter(httptest.NewRecorder()),
}

// Execute the middleware
Expand Down
15 changes: 13 additions & 2 deletions reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Reply struct {
redirect bool
redirectURL string
done bool
gzip bool
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Expand All @@ -33,6 +34,7 @@ func NewReply() *Reply {
return &Reply{
Hdr: http.Header{},
Code: http.StatusOK,
gzip: true,
}
}

Expand Down Expand Up @@ -314,6 +316,14 @@ func (r *Reply) Cookie(cookie *http.Cookie) *Reply {
return r
}

// DisableGzip method allows you disable Gzip for the reply. By default every
// response is gzip compressed if the client supports it and gzip enabled in
// app config.
func (r *Reply) DisableGzip() *Reply {
r.gzip = false
return r
}

// IsContentTypeSet method returns true if Content-Type is set otherwise
// false.
func (r *Reply) IsContentTypeSet() bool {
Expand All @@ -322,14 +332,15 @@ func (r *Reply) IsContentTypeSet() bool {

// Reset method resets the values into initialized state.
func (r *Reply) Reset() {
r.Code = 0
r.Code = http.StatusOK
r.ContType = ""
r.Hdr = nil
r.Hdr = http.Header{}
r.Rdr = nil
r.cookies = make([]*http.Cookie, 0)
r.redirect = false
r.redirectURL = ""
r.done = false
r.gzip = true
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Expand Down
29 changes: 21 additions & 8 deletions static.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ import (

// serveStatic method static file/directory delivery.
func (e *engine) serveStatic(ctx *Context) error {
res, req := ctx.Res, ctx.Req
res.Header().Set(ahttp.HeaderServer, aahServerName)

var fileabs string
if ctx.route.IsDir() {
fileabs = filepath.Join(AppBaseDir(), ctx.route.Dir, filepath.FromSlash(ctx.Req.Params.PathValue("filepath")))
Expand All @@ -34,9 +31,16 @@ func (e *engine) serveStatic(ctx *Context) error {
dir, file := filepath.Split(fileabs)
log.Tracef("Dir: %s, File: %s", dir, file)

ctx.Reply().gzip = checkGzipRequired(file)
if ctx.Req.IsGzipAccepted && e.isGzipEnabled && ctx.Reply().gzip {
e.wrapGzipWriter(ctx)
}

res, req := ctx.Res, ctx.Req
res.Header().Set(ahttp.HeaderServer, aahServerName)

fs := ahttp.Dir(dir, ctx.route.ListDir)
f, err := fs.Open(file)
defer ess.CloseQuietly(f)
if err != nil {
if err == ahttp.ErrDirListNotAllowed {
log.Warnf("directory listing not allowed: %s", req.Path)
Expand All @@ -57,6 +61,8 @@ func (e *engine) serveStatic(ctx *Context) error {
return nil
}

defer ess.CloseQuietly(f)

fi, err := f.Stat()
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
Expand All @@ -72,14 +78,10 @@ func (e *engine) serveStatic(ctx *Context) error {
return nil
}

e.writeGzipHeaders(ctx, false)

directoryList(res, req.Raw, f)
return nil
}

e.writeGzipHeaders(ctx, false)

http.ServeContent(res, req.Raw, file, fi.ModTime(), f)
return nil
}
Expand Down Expand Up @@ -122,6 +124,17 @@ func directoryList(res http.ResponseWriter, req *http.Request, f http.File) {
fmt.Fprintf(res, "</html>\n")
}

// checkGzipRequired method return for static which requires gzip response.
func checkGzipRequired(file string) bool {
switch filepath.Ext(file) {
case ".css", ".js", ".html", ".htm", ".json", ".xml",
".txt", ".csv", ".ttf", ".otf", ".eot":
return true
default:
return false
}
}

// Sort interface for Directory list
func (s byName) Len() int { return len(s) }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
Expand Down
5 changes: 1 addition & 4 deletions testdata/config/routes.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ domains {
name = "aahframework.org routes"
host = "localhost"

# For port 80, please assign 'port' field to empty string
port = "8080"

#-----------------------------------------------------------------
# To serve Static files. It can be directory or individual file.
# 'static' section is optional, if you don't have static files.
Expand Down Expand Up @@ -100,7 +97,7 @@ domains {
}

credits {
path = "/credits.html"
path = "/credits"
controller = "site.Site"
action = "Credits"
}
Expand Down
1 change: 1 addition & 0 deletions testdata/config/security.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
security {
session {
# defaults with take place
mode = "stateful"
}
}

0 comments on commit e7b04f2

Please sign in to comment.