@@ -5,11 +5,13 @@ package main
55
66import (
77 "bytes"
8+ "compress/gzip"
89 "context"
910 "encoding/base64"
1011 "encoding/json"
1112 "errors"
1213 "fmt"
14+ "io"
1315 "net/http"
1416 "sort"
1517 "strings"
@@ -43,37 +45,45 @@ func handleContext(fn contextHandler) http.Handler {
4345 }
4446 defer backpressureRobots (c , r )()
4547 }
46- if err := fn (c , w , r ); err != nil {
47- hdr := commonHeaderRaw (c , r )
48- data := & struct {
49- Header * uiHeader
50- Error string
51- TraceID string
52- }{
53- Header : hdr ,
54- Error : err .Error (),
55- TraceID : strings .Join (r .Header ["X-Cloud-Trace-Context" ], " " ),
56- }
57- if err == ErrAccess {
58- if hdr .LoginLink != "" {
59- http .Redirect (w , r , hdr .LoginLink , http .StatusTemporaryRedirect )
60- return
61- }
62- http .Error (w , "403 Forbidden" , http .StatusForbidden )
48+
49+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
50+ gzw := newGzipResponseWriterCloser (w )
51+ defer gzw .Close ()
52+ err := fn (c , gzw , r )
53+ if err == nil {
54+ if err = gzw .writeResult (r ); err == nil {
6355 return
6456 }
65- var redir * ErrRedirect
66- if errors .As (err , & redir ) {
67- http .Redirect (w , r , redir .Error (), http .StatusFound )
57+ }
58+ hdr := commonHeaderRaw (c , r )
59+ data := & struct {
60+ Header * uiHeader
61+ Error string
62+ TraceID string
63+ }{
64+ Header : hdr ,
65+ Error : err .Error (),
66+ TraceID : strings .Join (r .Header ["X-Cloud-Trace-Context" ], " " ),
67+ }
68+ if err == ErrAccess {
69+ if hdr .LoginLink != "" {
70+ http .Redirect (w , r , hdr .LoginLink , http .StatusTemporaryRedirect )
6871 return
6972 }
73+ http .Error (w , "403 Forbidden" , http .StatusForbidden )
74+ return
75+ }
76+ var redir * ErrRedirect
77+ if errors .As (err , & redir ) {
78+ http .Redirect (w , r , redir .Error (), http .StatusFound )
79+ return
80+ }
7081
71- status := logErrorPrepareStatus (c , err )
72- w .WriteHeader (status )
73- if err1 := templates .ExecuteTemplate (w , "error.html" , data ); err1 != nil {
74- combinedError := fmt .Sprintf ("got err \" %v\" processing ExecuteTemplate() for err \" %v\" " , err1 , err )
75- http .Error (w , combinedError , http .StatusInternalServerError )
76- }
82+ status := logErrorPrepareStatus (c , err )
83+ w .WriteHeader (status )
84+ if err1 := templates .ExecuteTemplate (w , "error.html" , data ); err1 != nil {
85+ combinedError := fmt .Sprintf ("got err \" %v\" processing ExecuteTemplate() for err \" %v\" " , err1 , err )
86+ http .Error (w , combinedError , http .StatusInternalServerError )
7787 }
7888 })
7989}
@@ -339,3 +349,64 @@ func encodeCookie(w http.ResponseWriter, cd *cookieData) {
339349}
340350
341351var templates = html .CreateGlob ("*.html" )
352+
353+ // gzipResponseWriterCloser accumulates the gzipped result.
354+ // In case of error during the handler processing, we'll drop this gzipped data.
355+ // It allows to call http.Error in the middle of the response generation.
356+ //
357+ // For 200 Ok responses we return the compressed data or decompress it depending on the client Accept-Encoding header.
358+ type gzipResponseWriterCloser struct {
359+ w * gzip.Writer
360+ plainResponseSize int
361+ buf * bytes.Buffer
362+ rw http.ResponseWriter
363+ }
364+
365+ func (g * gzipResponseWriterCloser ) Write (p []byte ) (n int , err error ) {
366+ g .plainResponseSize += len (p )
367+ return g .w .Write (p )
368+ }
369+
370+ func (g * gzipResponseWriterCloser ) Close () {
371+ if g .w != nil {
372+ g .w .Close ()
373+ }
374+ }
375+
376+ func (g * gzipResponseWriterCloser ) Header () http.Header {
377+ return g .rw .Header ()
378+ }
379+
380+ func (g * gzipResponseWriterCloser ) WriteHeader (statusCode int ) {
381+ g .rw .WriteHeader (statusCode )
382+ }
383+
384+ func (g * gzipResponseWriterCloser ) writeResult (r * http.Request ) error {
385+ g .w .Close ()
386+ g .w = nil
387+ clientSupportsGzip := strings .Contains (r .Header .Get ("Accept-Encoding" ), "gzip" )
388+ if clientSupportsGzip {
389+ g .rw .Header ().Set ("Content-Encoding" , "gzip" )
390+ _ , err := g .rw .Write (g .buf .Bytes ())
391+ return err
392+ }
393+ if g .plainResponseSize > 31 << 20 { // 32MB is the AppEngine hard limit for the response size.
394+ return fmt .Errorf ("len(response) > 31M, try to request gzipped: %w" , ErrClientBadRequest )
395+ }
396+ gzr , err := gzip .NewReader (g .buf )
397+ if err != nil {
398+ return fmt .Errorf ("gzip.NewReader: %w" , err )
399+ }
400+ defer gzr .Close ()
401+ _ , err = io .Copy (g .rw , gzr )
402+ return err
403+ }
404+
405+ func newGzipResponseWriterCloser (w http.ResponseWriter ) * gzipResponseWriterCloser {
406+ buf := & bytes.Buffer {}
407+ return & gzipResponseWriterCloser {
408+ w : gzip .NewWriter (buf ),
409+ buf : buf ,
410+ rw : w ,
411+ }
412+ }
0 commit comments