From 0bf0ff75670be4aadd1d9fc8f329c5f2a1a1e4f3 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 31 May 2021 12:55:26 +0200 Subject: [PATCH 01/14] Add gzip HTTP wrapper Fork and clean up+extend the dead `nytimes/gziphandler` project. Includes https://github.com/nytimes/gziphandler/pull/106 as well as support for stateless encoding. Removes testify from deps. --- gzhttp/LICENSE | 201 +++++++++ gzhttp/README.md | 54 +++ gzhttp/asserts_test.go | 69 +++ gzhttp/gzip.go | 519 ++++++++++++++++++++++ gzhttp/gzip_test.go | 651 ++++++++++++++++++++++++++++ gzhttp/writer/gzkp/gzkp.go | 68 +++ gzhttp/writer/gzkp/gzkp_test.go | 26 ++ gzhttp/writer/interface.go | 20 + gzhttp/writer/stdlib/stdlib.go | 68 +++ gzhttp/writer/stdlib/stdlib_test.go | 27 ++ 10 files changed, 1703 insertions(+) create mode 100644 gzhttp/LICENSE create mode 100644 gzhttp/README.md create mode 100644 gzhttp/asserts_test.go create mode 100644 gzhttp/gzip.go create mode 100644 gzhttp/gzip_test.go create mode 100644 gzhttp/writer/gzkp/gzkp.go create mode 100644 gzhttp/writer/gzkp/gzkp_test.go create mode 100644 gzhttp/writer/interface.go create mode 100644 gzhttp/writer/stdlib/stdlib.go create mode 100644 gzhttp/writer/stdlib/stdlib_test.go diff --git a/gzhttp/LICENSE b/gzhttp/LICENSE new file mode 100644 index 0000000000..df6192d36f --- /dev/null +++ b/gzhttp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016-2017 The New York Times Company + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gzhttp/README.md b/gzhttp/README.md new file mode 100644 index 0000000000..2816371274 --- /dev/null +++ b/gzhttp/README.md @@ -0,0 +1,54 @@ +Gzip Handler +============ + +This is a tiny Go package which wraps HTTP handlers to transparently gzip the +response body, for clients which support it. + +This package is forked from the [dead nytimes/gziphandler](https://github.com/nytimes/gziphandler) +and extends functionality for it. + +## Install +```bash +go get -u github.com/klauspost/compress +``` + +## Usage + +Call `GzipHandler` with any handler (an object which implements the +`http.Handler` interface), and it'll return a new handler which gzips the +response. For example: + +```go +package main + +import ( + "io" + "net/http" + "github.com/klauspost/compress/gzhttp" +) + +func main() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, "Hello, World") + }) + + http.Handle("/", gzhttp.GzipHandler(handler)) + http.ListenAndServe("0.0.0.0:8000", nil) +} +``` + + +## Documentation + +The docs can be found at [godoc.org][], as usual. + + +## License + +[Apache 2.0][license]. + + + +[docs]: https://godoc.org/github.com/NYTimes/gziphandler +[license]: https://github.com/NYTimes/gziphandler/blob/master/LICENSE diff --git a/gzhttp/asserts_test.go b/gzhttp/asserts_test.go new file mode 100644 index 0000000000..987b848ea0 --- /dev/null +++ b/gzhttp/asserts_test.go @@ -0,0 +1,69 @@ +package gziphandler + +import ( + "reflect" + "testing" +) + +func assertEqual(t testing.TB, want, got interface{}) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Fatalf("want %#v, got %#v", want, got) + } +} + +func assertNotEqual(t testing.TB, want, got interface{}) { + t.Helper() + if reflect.DeepEqual(want, got) { + t.Fatalf("want %#v, got %#v", want, got) + } +} + +func assertNil(t testing.TB, object interface{}) { + if isNil(object) { + return + } + t.Helper() + t.Fatalf("Expected value to be nil.") +} + +func assertNotNil(t testing.TB, object interface{}) { + if !isNil(object) { + return + } + t.Helper() + t.Fatalf("Expected value not to be nil.") +} + +// isNil checks if a specified object is nil or not, without Failing. +func isNil(object interface{}) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + kind := value.Kind() + isNilableKind := containsKind( + []reflect.Kind{ + reflect.Chan, reflect.Func, + reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice}, + kind) + + if isNilableKind && value.IsNil() { + return true + } + + return false +} + +// containsKind checks if a specified kind in the slice of kinds. +func containsKind(kinds []reflect.Kind, kind reflect.Kind) bool { + for i := 0; i < len(kinds); i++ { + if kind == kinds[i] { + return true + } + } + + return false +} diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go new file mode 100644 index 0000000000..dcca6288c8 --- /dev/null +++ b/gzhttp/gzip.go @@ -0,0 +1,519 @@ +package gziphandler + +import ( + "bufio" + "fmt" + "io" + "mime" + "net" + "net/http" + "strconv" + "strings" + + "github.com/klauspost/compress/gzhttp/writer" + "github.com/klauspost/compress/gzhttp/writer/gzkp" + "github.com/klauspost/compress/gzip" +) + +const ( + vary = "Vary" + acceptEncoding = "Accept-Encoding" + contentEncoding = "Content-Encoding" + contentType = "Content-Type" + contentLength = "Content-Length" +) + +type codings map[string]float64 + +const ( + // DefaultQValue is the default qvalue to assign to an encoding if no explicit qvalue is set. + // This is actually kind of ambiguous in RFC 2616, so hopefully it's correct. + // The examples seem to indicate that it is. + DefaultQValue = 1.0 + + // DefaultMinSize is the default minimum size until we enable gzip compression. + // 1500 bytes is the MTU size for the internet since that is the largest size allowed at the network layer. + // If you take a file that is 1300 bytes and compress it to 800 bytes, it’s still transmitted in that same 1500 byte packet regardless, so you’ve gained nothing. + // That being the case, you should restrict the gzip compression to files with a size greater than a single packet, 1400 bytes (1.4KB) is a safe value. + DefaultMinSize = 1400 +) + +// GzipResponseWriter provides an http.ResponseWriter interface, which gzips +// bytes before writing them to the underlying response. This doesn't close the +// writers, so don't forget to do that. +// It can be configured to skip response smaller than minSize. +type GzipResponseWriter struct { + http.ResponseWriter + level int + gwFactory writer.GzipWriterFactory + gw writer.GzipWriter + + code int // Saves the WriteHeader value. + + minSize int // Specifies the minimum response size to gzip. If the response length is bigger than this value, it is compressed. + buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. + ignore bool // If true, then we immediately passthru writes to the underlying ResponseWriter. + + contentTypes []parsedContentType // Only compress if the response is one of these content-types. All are accepted if empty. +} + +type GzipResponseWriterWithCloseNotify struct { + *GzipResponseWriter +} + +func (w GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool { + return w.ResponseWriter.(http.CloseNotifier).CloseNotify() +} + +// Write appends data to the gzip writer. +func (w *GzipResponseWriter) Write(b []byte) (int, error) { + // GZIP responseWriter is initialized. Use the GZIP responseWriter. + if w.gw != nil { + return w.gw.Write(b) + } + + // If we have already decided not to use GZIP, immediately passthrough. + if w.ignore { + return w.ResponseWriter.Write(b) + } + + // Save the write into a buffer for later use in GZIP responseWriter (if content is long enough) or at close with regular responseWriter. + // On the first write, w.buf changes from nil to a valid slice + w.buf = append(w.buf, b...) + + var ( + cl, _ = strconv.Atoi(w.Header().Get(contentLength)) + ct = w.Header().Get(contentType) + ce = w.Header().Get(contentEncoding) + ) + // Only continue if they didn't already choose an encoding or a known unhandled content length or type. + if ce == "" && (cl == 0 || cl >= w.minSize) && (ct == "" || handleContentType(w.contentTypes, ct)) { + // If the current buffer is less than minSize and a Content-Length isn't set, then wait until we have more data. + if len(w.buf) < w.minSize && cl == 0 { + return len(b), nil + } + // If the Content-Length is larger than minSize or the current buffer is larger than minSize, then continue. + if cl >= w.minSize || len(w.buf) >= w.minSize { + // If a Content-Type wasn't specified, infer it from the current buffer. + if ct == "" { + ct = http.DetectContentType(w.buf) + w.Header().Set(contentType, ct) + } + // If the Content-Type is acceptable to GZIP, initialize the GZIP writer. + if handleContentType(w.contentTypes, ct) { + if err := w.startGzip(); err != nil { + return 0, err + } + return len(b), nil + } + } + } + // If we got here, we should not GZIP this response. + if err := w.startPlain(); err != nil { + return 0, err + } + return len(b), nil +} + +// startGzip initializes a GZIP writer and writes the buffer. +func (w *GzipResponseWriter) startGzip() error { + // Set the GZIP header. + w.Header().Set(contentEncoding, "gzip") + + // if the Content-Length is already set, then calls to Write on gzip + // will fail to set the Content-Length header since its already set + // See: https://github.com/golang/go/issues/14975. + w.Header().Del(contentLength) + + // Write the header to gzip response. + if w.code != 0 { + w.ResponseWriter.WriteHeader(w.code) + // Ensure that no other WriteHeader's happen + w.code = 0 + } + + // Initialize and flush the buffer into the gzip response if there are any bytes. + // If there aren't any, we shouldn't initialize it yet because on Close it will + // write the gzip header even if nothing was ever written. + if len(w.buf) > 0 { + // Initialize the GZIP response. + w.init() + n, err := w.gw.Write(w.buf) + + // This should never happen (per io.Writer docs), but if the write didn't + // accept the entire buffer but returned no specific error, we have no clue + // what's going on, so abort just to be safe. + if err == nil && n < len(w.buf) { + err = io.ErrShortWrite + } + return err + } + return nil +} + +// startPlain writes to sent bytes and buffer the underlying ResponseWriter without gzip. +func (w *GzipResponseWriter) startPlain() error { + if w.code != 0 { + w.ResponseWriter.WriteHeader(w.code) + // Ensure that no other WriteHeader's happen + w.code = 0 + } + w.ignore = true + // If Write was never called then don't call Write on the underlying ResponseWriter. + if w.buf == nil { + return nil + } + n, err := w.ResponseWriter.Write(w.buf) + w.buf = nil + // This should never happen (per io.Writer docs), but if the write didn't + // accept the entire buffer but returned no specific error, we have no clue + // what's going on, so abort just to be safe. + if err == nil && n < len(w.buf) { + err = io.ErrShortWrite + } + return err +} + +// WriteHeader just saves the response code until close or GZIP effective writes. +func (w *GzipResponseWriter) WriteHeader(code int) { + if w.code == 0 { + w.code = code + } +} + +// init graps a new gzip writer from the gzipWriterPool and writes the correct +// content encoding header. +func (w *GzipResponseWriter) init() { + // Bytes written during ServeHTTP are redirected to this gzip writer + // before being written to the underlying response. + w.gw = w.gwFactory.New(w.ResponseWriter, w.level) +} + +// Close will close the gzip.Writer and will put it back in the gzipWriterPool. +func (w *GzipResponseWriter) Close() error { + if w.ignore { + return nil + } + + if w.gw == nil { + // GZIP not triggered yet, write out regular response. + err := w.startPlain() + // Returns the error if any at write. + if err != nil { + err = fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", err.Error()) + } + return err + } + + err := w.gw.Close() + w.gw = nil + return err +} + +// Flush flushes the underlying *gzip.Writer and then the underlying +// http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter +// an http.Flusher. +func (w *GzipResponseWriter) Flush() { + if w.gw == nil && !w.ignore { + // Only flush once startGzip or startPlain has been called. + // + // Flush is thus a no-op until we're certain whether a plain + // or gzipped response will be served. + return + } + + if w.gw != nil { + w.gw.Flush() + } + + if fw, ok := w.ResponseWriter.(http.Flusher); ok { + fw.Flush() + } +} + +// Hijack implements http.Hijacker. If the underlying ResponseWriter is a +// Hijacker, its Hijack method is returned. Otherwise an error is returned. +func (w *GzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hj, ok := w.ResponseWriter.(http.Hijacker); ok { + return hj.Hijack() + } + return nil, nil, fmt.Errorf("http.Hijacker interface is not supported") +} + +// verify Hijacker interface implementation +var _ http.Hijacker = &GzipResponseWriter{} + +// MustNewGzipLevelHandler behaves just like NewGzipLevelHandler except that in +// an error case it panics rather than returning an error. +func MustNewGzipLevelHandler(level int) func(http.Handler) http.Handler { + wrap, err := NewGzipHandler(CompressionLevel(level)) + if err != nil { + panic(err) + } + return wrap +} + +// NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller +// specify the minimum size before compression. +func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) { + return NewGzipHandler(CompressionLevel(level), MinSize(minSize)) +} + +func NewGzipHandler(opts ...option) (func(http.Handler) http.Handler, error) { + c := &config{ + level: gzip.DefaultCompression, + minSize: DefaultMinSize, + writer: writer.GzipWriterFactory{ + Levels: gzkp.Levels, + New: gzkp.NewWriter, + }, + } + + for _, o := range opts { + o(c) + } + + if err := c.validate(); err != nil { + return nil, err + } + + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add(vary, acceptEncoding) + if acceptsGzip(r) { + gw := &GzipResponseWriter{ + ResponseWriter: w, + gwFactory: c.writer, + level: c.level, + minSize: c.minSize, + contentTypes: c.contentTypes, + } + defer gw.Close() + + if _, ok := w.(http.CloseNotifier); ok { + gwcn := GzipResponseWriterWithCloseNotify{gw} + h.ServeHTTP(gwcn, r) + } else { + h.ServeHTTP(gw, r) + } + + } else { + h.ServeHTTP(w, r) + } + }) + }, nil +} + +// Parsed representation of one of the inputs to ContentTypes. +// See https://golang.org/pkg/mime/#ParseMediaType +type parsedContentType struct { + mediaType string + params map[string]string +} + +// equals returns whether this content type matches another content type. +func (pct parsedContentType) equals(mediaType string, params map[string]string) bool { + if pct.mediaType != mediaType { + return false + } + // if pct has no params, don't care about other's params + if len(pct.params) == 0 { + return true + } + + // if pct has any params, they must be identical to other's. + if len(pct.params) != len(params) { + return false + } + for k, v := range pct.params { + if w, ok := params[k]; !ok || v != w { + return false + } + } + return true +} + +// Used for functional configuration. +type config struct { + minSize int + level int + writer writer.GzipWriterFactory + contentTypes []parsedContentType +} + +func (c *config) validate() error { + min, max := c.writer.Levels() + if c.level < min || c.level > max { + return fmt.Errorf("invalid compression level requested: %d, valid range %d -> %d", c.level, min, max) + } + + if c.minSize < 0 { + return fmt.Errorf("minimum size must be more than zero") + } + + return nil +} + +type option func(c *config) + +func MinSize(size int) option { + return func(c *config) { + c.minSize = size + } +} + +// CompressionLevel sets the compression level +func CompressionLevel(level int) option { + return func(c *config) { + c.level = level + } +} + +// Implementation changes the implementation of GzipWriter +// +// The default implementation is writer/stdlib/NewWriter +// which is backed by standard library's compress/zlib +func Implementation(writer writer.GzipWriterFactory) option { + return func(c *config) { + c.writer = writer + } +} + +// ContentTypes specifies a list of content types to compare +// the Content-Type header to before compressing. If none +// match, the response will be returned as-is. +// +// Content types are compared in a case-insensitive, whitespace-ignored +// manner. +// +// A MIME type without any other directive will match a content type +// that has the same MIME type, regardless of that content type's other +// directives. I.e., "text/html" will match both "text/html" and +// "text/html; charset=utf-8". +// +// A MIME type with any other directive will only match a content type +// that has the same MIME type and other directives. I.e., +// "text/html; charset=utf-8" will only match "text/html; charset=utf-8". +// +// By default, responses are gzipped regardless of +// Content-Type. +func ContentTypes(types []string) option { + return func(c *config) { + c.contentTypes = []parsedContentType{} + for _, v := range types { + mediaType, params, err := mime.ParseMediaType(v) + if err == nil { + c.contentTypes = append(c.contentTypes, parsedContentType{mediaType, params}) + } + } + } +} + +/* +func ContentTypeFilter(func(contentType string) bool) { + return func(c *config) { + c.contentTypes = []parsedContentType{} + for _, v := range types { + mediaType, params, err := mime.ParseMediaType(v) + if err == nil { + c.contentTypes = append(c.contentTypes, parsedContentType{mediaType, params}) + } + } + } +} +*/ + +// GzipHandler wraps an HTTP handler, to transparently gzip the response body if +// the client supports it (via the Accept-Encoding header). This will compress at +// the default compression level. +func GzipHandler(h http.Handler, opts ...option) http.Handler { + wrapper, _ := NewGzipHandler(opts...) + return wrapper(h) +} + +// acceptsGzip returns true if the given HTTP request indicates that it will +// accept a gzipped response. +func acceptsGzip(r *http.Request) bool { + acceptedEncodings, _ := parseEncodings(r.Header.Get(acceptEncoding)) + return acceptedEncodings["gzip"] > 0.0 +} + +// returns true if we've been configured to compress the specific content type. +func handleContentType(contentTypes []parsedContentType, ct string) bool { + // If contentTypes is empty we handle all content types. + if len(contentTypes) == 0 { + return true + } + + mediaType, params, err := mime.ParseMediaType(ct) + if err != nil { + return false + } + + for _, c := range contentTypes { + if c.equals(mediaType, params) { + return true + } + } + + return false +} + +// parseEncodings attempts to parse a list of codings, per RFC 2616, as might +// appear in an Accept-Encoding header. It returns a map of content-codings to +// quality values, and an error containing the errors encountered. It's probably +// safe to ignore those, because silently ignoring errors is how the internet +// works. +// +// See: http://tools.ietf.org/html/rfc2616#section-14.3. +func parseEncodings(s string) (codings, error) { + split := strings.Split(s, ",") + c := make(codings, len(split)) + var e []string + + for _, ss := range split { + coding, qvalue, err := parseCoding(ss) + + if err != nil { + e = append(e, err.Error()) + } else { + c[coding] = qvalue + } + } + + // TODO (adammck): Use a proper multi-error struct, so the individual errors + // can be extracted if anyone cares. + if len(e) > 0 { + return c, fmt.Errorf("errors while parsing encodings: %s", strings.Join(e, ", ")) + } + + return c, nil +} + +// parseCoding parses a single conding (content-coding with an optional qvalue), +// as might appear in an Accept-Encoding header. It attempts to forgive minor +// formatting errors. +func parseCoding(s string) (coding string, qvalue float64, err error) { + for n, part := range strings.Split(s, ";") { + part = strings.TrimSpace(part) + qvalue = DefaultQValue + + if n == 0 { + coding = strings.ToLower(part) + } else if strings.HasPrefix(part, "q=") { + qvalue, err = strconv.ParseFloat(strings.TrimPrefix(part, "q="), 64) + + if qvalue < 0.0 { + qvalue = 0.0 + } else if qvalue > 1.0 { + qvalue = 1.0 + } + } + } + + if coding == "" { + err = fmt.Errorf("empty content-coding") + } + + return +} diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go new file mode 100644 index 0000000000..ca5ab9f909 --- /dev/null +++ b/gzhttp/gzip_test.go @@ -0,0 +1,651 @@ +package gziphandler + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/klauspost/compress/gzip" +) + +const ( + smallTestBody = "aaabbcaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbc" + testBody = "aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc" +) + +func TestParseEncodings(t *testing.T) { + examples := map[string]codings{ + + // Examples from RFC 2616 + "compress, gzip": {"compress": 1.0, "gzip": 1.0}, + "": {}, + "*": {"*": 1.0}, + "compress;q=0.5, gzip;q=1.0": {"compress": 0.5, "gzip": 1.0}, + "gzip;q=1.0, identity; q=0.5, *;q=0": {"gzip": 1.0, "identity": 0.5, "*": 0.0}, + + // More random stuff + "AAA;q=1": {"aaa": 1.0}, + "BBB ; q = 2": {"bbb": 1.0}, + } + + for eg, exp := range examples { + act, _ := parseEncodings(eg) + assertEqual(t, exp, act) + } +} + +func TestGzipHandler(t *testing.T) { + // This just exists to provide something for GzipHandler to wrap. + handler := newTestHandler(testBody) + + // requests without accept-encoding are passed along as-is + + req1, _ := http.NewRequest("GET", "/whatever", nil) + resp1 := httptest.NewRecorder() + handler.ServeHTTP(resp1, req1) + res1 := resp1.Result() + + assertEqual(t, 200, res1.StatusCode) + assertEqual(t, "", res1.Header.Get("Content-Encoding")) + assertEqual(t, "Accept-Encoding", res1.Header.Get("Vary")) + assertEqual(t, testBody, resp1.Body.String()) + + // but requests with accept-encoding:gzip are compressed if possible + + req2, _ := http.NewRequest("GET", "/whatever", nil) + req2.Header.Set("Accept-Encoding", "gzip") + resp2 := httptest.NewRecorder() + handler.ServeHTTP(resp2, req2) + res2 := resp2.Result() + + assertEqual(t, 200, res2.StatusCode) + assertEqual(t, "gzip", res2.Header.Get("Content-Encoding")) + assertEqual(t, "Accept-Encoding", res2.Header.Get("Vary")) + assertEqual(t, gzipStrLevel(testBody, gzip.DefaultCompression), resp2.Body.Bytes()) + + // content-type header is correctly set based on uncompressed body + + req3, _ := http.NewRequest("GET", "/whatever", nil) + req3.Header.Set("Accept-Encoding", "gzip") + res3 := httptest.NewRecorder() + handler.ServeHTTP(res3, req3) + + assertEqual(t, http.DetectContentType([]byte(testBody)), res3.Header().Get("Content-Type")) +} + +func TestGzipHandlerSmallBodyNoCompression(t *testing.T) { + handler := newTestHandler(smallTestBody) + + req, _ := http.NewRequest("GET", "/whatever", nil) + req.Header.Set("Accept-Encoding", "gzip") + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + res := resp.Result() + + // with less than 1400 bytes the response should not be gzipped + + assertEqual(t, 200, res.StatusCode) + assertEqual(t, "", res.Header.Get("Content-Encoding")) + assertEqual(t, "Accept-Encoding", res.Header.Get("Vary")) + assertEqual(t, smallTestBody, resp.Body.String()) + +} + +func TestGzipHandlerAlreadyCompressed(t *testing.T) { + handler := newTestHandler(testBody) + + req, _ := http.NewRequest("GET", "/gzipped", nil) + req.Header.Set("Accept-Encoding", "gzip") + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + + assertEqual(t, testBody, res.Body.String()) +} + +func TestNewGzipLevelHandler(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + io.WriteString(w, testBody) + }) + + for lvl := gzip.StatelessCompression; lvl <= gzip.BestCompression; lvl++ { + t.Run(fmt.Sprint(lvl), func(t *testing.T) { + wrapper, err := NewGzipHandler(CompressionLevel(lvl)) + assertNil(t, err) + + req, _ := http.NewRequest("GET", "/whatever", nil) + req.Header.Set("Accept-Encoding", "gzip") + resp := httptest.NewRecorder() + wrapper(handler).ServeHTTP(resp, req) + res := resp.Result() + + assertEqual(t, 200, res.StatusCode) + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + assertEqual(t, "Accept-Encoding", res.Header.Get("Vary")) + got := gzipStrLevel(testBody, lvl) + assertEqual(t, got, resp.Body.Bytes()) + t.Log(lvl, len(got)) + }) + } +} + +func TestNewGzipLevelHandlerReturnsErrorForInvalidLevels(t *testing.T) { + var err error + _, err = NewGzipHandler(CompressionLevel(-42)) + assertNotNil(t, err) + + _, err = NewGzipHandler(CompressionLevel(42)) + assertNotNil(t, err) +} + +func TestMustNewGzipLevelHandlerWillPanic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("panic was not called") + } + }() + + _ = MustNewGzipLevelHandler(-42) +} + +func TestGzipHandlerNoBody(t *testing.T) { + tests := []struct { + statusCode int + contentEncoding string + emptyBody bool + body []byte + }{ + // Body must be empty. + {http.StatusNoContent, "", true, nil}, + {http.StatusNotModified, "", true, nil}, + // Body is going to get gzip'd no matter what. + {http.StatusOK, "", true, []byte{}}, + {http.StatusOK, "gzip", false, []byte(testBody)}, + } + + for num, test := range tests { + t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) { + handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.statusCode) + if test.body != nil { + w.Write(test.body) + } + })) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "gzip") + handler.ServeHTTP(rec, req) + + body, err := ioutil.ReadAll(rec.Body) + if err != nil { + t.Fatalf("Unexpected error reading response body: %v", err) + } + + header := rec.Header() + assertEqual(t, test.contentEncoding, header.Get("Content-Encoding")) + assertEqual(t, "Accept-Encoding", header.Get("Vary")) + if test.emptyBody { + assertEqual(t, 0, len(body)) + } else { + assertNotEqual(t, 0, len(body)) + assertNotEqual(t, test.body, body) + } + }) + + } +} + +func TestGzipHandlerContentLength(t *testing.T) { + testBodyBytes := []byte(testBody) + tests := []struct { + bodyLen int + bodies [][]byte + emptyBody bool + }{ + {len(testBody), [][]byte{testBodyBytes}, false}, + // each of these writes is less than the DefaultMinSize + {len(testBody), [][]byte{testBodyBytes[:200], testBodyBytes[200:]}, false}, + // without a defined Content-Length it should still gzip + {0, [][]byte{testBodyBytes[:200], testBodyBytes[200:]}, false}, + // simulate a HEAD request with an empty write (to populate headers) + {len(testBody), [][]byte{nil}, true}, + } + + // httptest.NewRecorder doesn't give you access to the Content-Length + // header so instead, we create a server on a random port and make + // a request to that instead + ln, err := net.Listen("tcp", "127.0.0.1:") + if err != nil { + t.Fatalf("failed creating listen socket: %v", err) + } + defer ln.Close() + srv := &http.Server{ + Handler: nil, + } + go srv.Serve(ln) + + for num, test := range tests { + t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) { + srv.Handler = GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if test.bodyLen > 0 { + w.Header().Set("Content-Length", strconv.Itoa(test.bodyLen)) + } + for _, b := range test.bodies { + w.Write(b) + } + })) + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/", Scheme: "http", Host: ln.Addr().String()}, + Header: make(http.Header), + Close: true, + } + req.Header.Set("Accept-Encoding", "gzip") + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("Unexpected error making http request in test iteration %d: %v", num, err) + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("Unexpected error reading response body in test iteration %d: %v", num, err) + } + + l, err := strconv.Atoi(res.Header.Get("Content-Length")) + if err != nil { + t.Fatalf("Unexpected error parsing Content-Length in test iteration %d: %v", num, err) + } + if test.emptyBody { + assertEqual(t, 0, len(body)) + assertEqual(t, 0, l) + } else { + assertEqual(t, len(body), l) + } + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + assertNotEqual(t, test.bodyLen, l) + }) + } +} + +func TestGzipHandlerMinSizeMustBePositive(t *testing.T) { + _, err := NewGzipLevelAndMinSize(gzip.DefaultCompression, -1) + assertNotNil(t, err) +} + +func TestGzipHandlerMinSize(t *testing.T) { + responseLength := 0 + b := []byte{'x'} + + wrapper, _ := NewGzipLevelAndMinSize(gzip.DefaultCompression, 128) + handler := wrapper(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // Write responses one byte at a time to ensure that the flush + // mechanism, if used, is working properly. + for i := 0; i < responseLength; i++ { + n, err := w.Write(b) + assertEqual(t, 1, n) + assertNil(t, err) + } + }, + )) + + r, _ := http.NewRequest("GET", "/whatever", &bytes.Buffer{}) + r.Header.Add("Accept-Encoding", "gzip") + + // Short response is not compressed + responseLength = 127 + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + if w.Result().Header.Get(contentEncoding) == "gzip" { + t.Error("Expected uncompressed response, got compressed") + } + + // Long response is not compressed + responseLength = 128 + w = httptest.NewRecorder() + handler.ServeHTTP(w, r) + if w.Result().Header.Get(contentEncoding) != "gzip" { + t.Error("Expected compressed response, got uncompressed") + } +} + +type panicOnSecondWriteHeaderWriter struct { + http.ResponseWriter + headerWritten bool +} + +func (w *panicOnSecondWriteHeaderWriter) WriteHeader(s int) { + if w.headerWritten { + panic("header already written") + } + w.headerWritten = true + w.ResponseWriter.WriteHeader(s) +} + +func TestGzipHandlerDoubleWriteHeader(t *testing.T) { + handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", "15000") + // Specifically write the header here + w.WriteHeader(304) + // Ensure that after a Write the header isn't triggered again on close + w.Write(nil) + })) + wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w = &panicOnSecondWriteHeaderWriter{ + ResponseWriter: w, + } + handler.ServeHTTP(w, r) + }) + + rec := httptest.NewRecorder() + // TODO: in Go1.7 httptest.NewRequest was introduced this should be used + // once 1.6 is not longer supported. + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/"}, + Proto: "HTTP/1.1", + ProtoMinor: 1, + RemoteAddr: "192.0.2.1:1234", + Header: make(http.Header), + } + req.Header.Set("Accept-Encoding", "gzip") + wrapper.ServeHTTP(rec, req) + body, err := ioutil.ReadAll(rec.Body) + if err != nil { + t.Fatalf("Unexpected error reading response body: %v", err) + } + assertEqual(t, 0, len(body)) + header := rec.Header() + assertEqual(t, "gzip", header.Get("Content-Encoding")) + assertEqual(t, "Accept-Encoding", header.Get("Vary")) + assertEqual(t, 304, rec.Code) +} + +func TestStatusCodes(t *testing.T) { + handler := GzipHandler(http.NotFoundHandler()) + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "gzip") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + result := w.Result() + if result.StatusCode != 404 { + t.Errorf("StatusCode should have been 404 but was %d", result.StatusCode) + } +} + +func TestFlushBeforeWrite(t *testing.T) { + b := []byte(testBody) + handler := GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusNotFound) + rw.(http.Flusher).Flush() + rw.Write(b) + })) + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("Accept-Encoding", "gzip") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + res := w.Result() + assertEqual(t, http.StatusNotFound, res.StatusCode) + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + assertNotEqual(t, b, w.Body.Bytes()) +} + +func TestImplementCloseNotifier(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, "/", nil) + request.Header.Set(acceptEncoding, "gzip") + GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + _, ok := rw.(http.CloseNotifier) + // response writer must implement http.CloseNotifier + assertEqual(t, true, ok) + })).ServeHTTP(&mockRWCloseNotify{}, request) +} + +func TestImplementFlusherAndCloseNotifier(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, "/", nil) + request.Header.Set(acceptEncoding, "gzip") + GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + _, okCloseNotifier := rw.(http.CloseNotifier) + // response writer must implement http.CloseNotifier + assertEqual(t, true, okCloseNotifier) + _, okFlusher := rw.(http.Flusher) + // "response writer must implement http.Flusher" + assertEqual(t, true, okFlusher) + })).ServeHTTP(&mockRWCloseNotify{}, request) +} + +func TestNotImplementCloseNotifier(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, "/", nil) + request.Header.Set(acceptEncoding, "gzip") + GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + _, ok := rw.(http.CloseNotifier) + // response writer must not implement http.CloseNotifier + assertEqual(t, false, ok) + })).ServeHTTP(httptest.NewRecorder(), request) +} + +type mockRWCloseNotify struct{} + +func (m *mockRWCloseNotify) CloseNotify() <-chan bool { + panic("implement me") +} + +func (m *mockRWCloseNotify) Header() http.Header { + return http.Header{} +} + +func (m *mockRWCloseNotify) Write([]byte) (int, error) { + panic("implement me") +} + +func (m *mockRWCloseNotify) WriteHeader(int) { + panic("implement me") +} + +func TestIgnoreSubsequentWriteHeader(t *testing.T) { + handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + w.WriteHeader(404) + })) + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "gzip") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + result := w.Result() + if result.StatusCode != 500 { + t.Errorf("StatusCode should have been 500 but was %d", result.StatusCode) + } +} + +func TestDontWriteWhenNotWrittenTo(t *testing.T) { + // When using gzip as middleware without ANY writes in the handler, + // ensure the gzip middleware doesn't touch the actual ResponseWriter + // either. + + handler0 := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + })) + + handler1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler0.ServeHTTP(w, r) + w.WriteHeader(404) // this only works if gzip didn't do a WriteHeader(200) + }) + + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "gzip") + w := httptest.NewRecorder() + handler1.ServeHTTP(w, r) + + result := w.Result() + if result.StatusCode != 404 { + t.Errorf("StatusCode should have been 404 but was %d", result.StatusCode) + } +} + +var contentTypeTests = []struct { + name string + contentType string + acceptedContentTypes []string + expectedGzip bool +}{ + { + name: "Always gzip when content types are empty", + contentType: "", + acceptedContentTypes: []string{}, + expectedGzip: true, + }, + { + name: "MIME match", + contentType: "application/json", + acceptedContentTypes: []string{"application/json"}, + expectedGzip: true, + }, + { + name: "MIME no match", + contentType: "text/xml", + acceptedContentTypes: []string{"application/json"}, + expectedGzip: false, + }, + { + name: "MIME match with no other directive ignores non-MIME directives", + contentType: "application/json; charset=utf-8", + acceptedContentTypes: []string{"application/json"}, + expectedGzip: true, + }, + { + name: "MIME match with other directives requires all directives be equal, different charset", + contentType: "application/json; charset=ascii", + acceptedContentTypes: []string{"application/json; charset=utf-8"}, + expectedGzip: false, + }, + { + name: "MIME match with other directives requires all directives be equal, same charset", + contentType: "application/json; charset=utf-8", + acceptedContentTypes: []string{"application/json; charset=utf-8"}, + expectedGzip: true, + }, + { + name: "MIME match with other directives requires all directives be equal, missing charset", + contentType: "application/json", + acceptedContentTypes: []string{"application/json; charset=ascii"}, + expectedGzip: false, + }, + { + name: "MIME match case insensitive", + contentType: "Application/Json", + acceptedContentTypes: []string{"application/json"}, + expectedGzip: true, + }, + { + name: "MIME match ignore whitespace", + contentType: "application/json;charset=utf-8", + acceptedContentTypes: []string{"application/json; charset=utf-8"}, + expectedGzip: true, + }, +} + +func TestContentTypes(t *testing.T) { + for _, tt := range contentTypeTests { + t.Run(tt.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", tt.contentType) + io.WriteString(w, testBody) + }) + + wrapper, err := NewGzipHandler(ContentTypes(tt.acceptedContentTypes)) + assertNil(t, err) + + req, _ := http.NewRequest("GET", "/whatever", nil) + req.Header.Set("Accept-Encoding", "gzip") + resp := httptest.NewRecorder() + wrapper(handler).ServeHTTP(resp, req) + res := resp.Result() + + assertEqual(t, 200, res.StatusCode) + if tt.expectedGzip { + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + } else { + assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding")) + } + }) + } +} + +// -------------------------------------------------------------------- + +func BenchmarkGzipHandler_S2k(b *testing.B) { benchmark(b, false, 2048) } +func BenchmarkGzipHandler_S20k(b *testing.B) { benchmark(b, false, 20480) } +func BenchmarkGzipHandler_S100k(b *testing.B) { benchmark(b, false, 102400) } +func BenchmarkGzipHandler_P2k(b *testing.B) { benchmark(b, true, 2048) } +func BenchmarkGzipHandler_P20k(b *testing.B) { benchmark(b, true, 20480) } +func BenchmarkGzipHandler_P100k(b *testing.B) { benchmark(b, true, 102400) } + +// -------------------------------------------------------------------- + +func gzipStrLevel(s string, lvl int) []byte { + var b bytes.Buffer + w, _ := gzip.NewWriterLevel(&b, lvl) + io.WriteString(w, s) + w.Close() + return b.Bytes() +} + +func benchmark(b *testing.B, parallel bool, size int) { + bin, err := ioutil.ReadFile("testdata/benchmark.json") + if err != nil { + b.Fatal(err) + } + + req, _ := http.NewRequest("GET", "/whatever", nil) + req.Header.Set("Accept-Encoding", "gzip") + handler := newTestHandler(string(bin[:size])) + + b.ReportAllocs() + b.SetBytes(int64(size)) + if parallel { + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + runBenchmark(b, req, handler) + } + }) + } else { + b.ResetTimer() + for i := 0; i < b.N; i++ { + runBenchmark(b, req, handler) + } + } +} + +func runBenchmark(b *testing.B, req *http.Request, handler http.Handler) { + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + if code := res.Code; code != 200 { + b.Fatalf("Expected 200 but got %d", code) + } else if blen := res.Body.Len(); blen < 500 { + b.Fatalf("Expected complete response body, but got %d bytes", blen) + } +} + +func newTestHandler(body string) http.Handler { + return GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/gzipped": + w.Header().Set("Content-Encoding", "gzip") + io.WriteString(w, body) + default: + io.WriteString(w, body) + } + })) +} diff --git a/gzhttp/writer/gzkp/gzkp.go b/gzhttp/writer/gzkp/gzkp.go new file mode 100644 index 0000000000..ed01074253 --- /dev/null +++ b/gzhttp/writer/gzkp/gzkp.go @@ -0,0 +1,68 @@ +package gzkp + +import ( + "io" + "sync" + + "github.com/klauspost/compress/gzhttp/writer" + "github.com/klauspost/compress/gzip" +) + +// gzipWriterPools stores a sync.Pool for each compression level for reuse of +// gzip.Writers. Use poolIndex to covert a compression level to an index into +// gzipWriterPools. +var gzipWriterPools [gzip.BestCompression - gzip.StatelessCompression + 1]*sync.Pool + +func init() { + for i := gzip.StatelessCompression; i <= gzip.BestCompression; i++ { + addLevelPool(i) + } +} + +// poolIndex maps a compression level to its index into gzipWriterPools. It +// assumes that level is a valid gzip compression level. +func poolIndex(level int) int { + return level - gzip.StatelessCompression +} + +func addLevelPool(level int) { + gzipWriterPools[poolIndex(level)] = &sync.Pool{ + New: func() interface{} { + // NewWriterLevel only returns error on a bad level, we are guaranteeing + // that this will be a valid level so it is okay to ignore the returned + // error. + w, _ := gzip.NewWriterLevel(nil, level) + return w + }, + } +} + +type pooledWriter struct { + *gzip.Writer + index int +} + +func (pw *pooledWriter) Close() error { + err := pw.Writer.Close() + gzipWriterPools[pw.index].Put(pw.Writer) + pw.Writer = nil + return err +} + +func NewWriter(w io.Writer, level int) writer.GzipWriter { + index := poolIndex(level) + gzw := gzipWriterPools[index].Get().(*gzip.Writer) + gzw.Reset(w) + return &pooledWriter{ + Writer: gzw, + index: index, + } +} + +func Levels() (min, max int) { + return gzip.StatelessCompression, gzip.BestCompression +} + +func ImplementationInfo() string { + return "klauspost/compress/gzip" +} diff --git a/gzhttp/writer/gzkp/gzkp_test.go b/gzhttp/writer/gzkp/gzkp_test.go new file mode 100644 index 0000000000..467255ee84 --- /dev/null +++ b/gzhttp/writer/gzkp/gzkp_test.go @@ -0,0 +1,26 @@ +package gzkp + +import ( + "bytes" + "compress/gzip" + "testing" +) + +func TestGzipDoubleClose(t *testing.T) { + // reset the pool for the default compression so we can make sure duplicates + // aren't added back by double close + addLevelPool(gzip.DefaultCompression) + + w := bytes.NewBufferString("") + writer := NewWriter(w, gzip.DefaultCompression) + writer.Close() + + // the second close shouldn't have added the same writer + // so we pull out 2 writers from the pool and make sure they're different + w1 := gzipWriterPools[poolIndex(gzip.DefaultCompression)].Get() + w2 := gzipWriterPools[poolIndex(gzip.DefaultCompression)].Get() + + if w1 == w2 { + t.Fatal("got same writer") + } +} diff --git a/gzhttp/writer/interface.go b/gzhttp/writer/interface.go new file mode 100644 index 0000000000..04c739d4ef --- /dev/null +++ b/gzhttp/writer/interface.go @@ -0,0 +1,20 @@ +package writer + +import "io" + +// GzipWriter implements the functions needed for compressing content. +type GzipWriter interface { + Close() error + Flush() error + Write(p []byte) (int, error) +} + +// GzipWriterFactory contains the information needed for the +type GzipWriterFactory struct { + // Must return the minimum and maximum supported level. + Levels func() (min, max int) + + // New must return a new GzipWriter. + // level will always be within the return limits above. + New func(writer io.Writer, level int) GzipWriter +} diff --git a/gzhttp/writer/stdlib/stdlib.go b/gzhttp/writer/stdlib/stdlib.go new file mode 100644 index 0000000000..f646eba297 --- /dev/null +++ b/gzhttp/writer/stdlib/stdlib.go @@ -0,0 +1,68 @@ +package gzkp + +import ( + "compress/gzip" + "io" + "sync" + + "github.com/klauspost/compress/gzhttp/writer" +) + +// gzipWriterPools stores a sync.Pool for each compression level for reuse of +// gzip.Writers. Use poolIndex to covert a compression level to an index into +// gzipWriterPools. +var gzipWriterPools [gzip.BestCompression - gzip.HuffmanOnly + 1]*sync.Pool + +func init() { + for i := gzip.HuffmanOnly; i <= gzip.BestCompression; i++ { + addLevelPool(i) + } +} + +// poolIndex maps a compression level to its index into gzipWriterPools. It +// assumes that level is a valid gzip compression level. +func poolIndex(level int) int { + return level - gzip.HuffmanOnly +} + +func addLevelPool(level int) { + gzipWriterPools[poolIndex(level)] = &sync.Pool{ + New: func() interface{} { + // NewWriterLevel only returns error on a bad level, we are guaranteeing + // that this will be a valid level so it is okay to ignore the returned + // error. + w, _ := gzip.NewWriterLevel(nil, level) + return w + }, + } +} + +type pooledWriter struct { + *gzip.Writer + index int +} + +func (pw *pooledWriter) Close() error { + err := pw.Writer.Close() + gzipWriterPools[pw.index].Put(pw.Writer) + pw.Writer = nil + return err +} + +func NewWriter(w io.Writer, level int) writer.GzipWriter { + index := poolIndex(level) + gzw := gzipWriterPools[index].Get().(*gzip.Writer) + gzw.Reset(w) + return &pooledWriter{ + Writer: gzw, + index: index, + } +} + +func Levels() (min, max int) { + return gzip.HuffmanOnly, gzip.BestCompression +} + +func ImplementationInfo() string { + return "compress/gzip" +} diff --git a/gzhttp/writer/stdlib/stdlib_test.go b/gzhttp/writer/stdlib/stdlib_test.go new file mode 100644 index 0000000000..6782f97bf6 --- /dev/null +++ b/gzhttp/writer/stdlib/stdlib_test.go @@ -0,0 +1,27 @@ +package gzkp + +import ( + "bytes" + "compress/gzip" + "testing" +) + +func TestGzipDoubleClose(t *testing.T) { + // reset the pool for the default compression so we can make sure duplicates + // aren't added back by double close + addLevelPool(gzip.DefaultCompression) + + w := bytes.NewBufferString("") + writer := NewWriter(w, gzip.DefaultCompression) + writer.Close() + + // the second close shouldn't have added the same writer + // so we pull out 2 writers from the pool and make sure they're different + w1 := gzipWriterPools[poolIndex(gzip.DefaultCompression)].Get() + w2 := gzipWriterPools[poolIndex(gzip.DefaultCompression)].Get() + + if w1 == w2 { + t.Fatal("got same writer") + } + +} From 4c42a46486982534b5621e39c1c76195617bf05e Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 31 May 2021 13:01:08 +0200 Subject: [PATCH 02/14] Add https://github.com/nytimes/gziphandler/pull/96 --- gzhttp/asserts_test.go | 2 +- gzhttp/gzip.go | 6 ++++++ gzhttp/gzip_test.go | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/gzhttp/asserts_test.go b/gzhttp/asserts_test.go index 987b848ea0..5bbfd80227 100644 --- a/gzhttp/asserts_test.go +++ b/gzhttp/asserts_test.go @@ -15,7 +15,7 @@ func assertEqual(t testing.TB, want, got interface{}) { func assertNotEqual(t testing.TB, want, got interface{}) { t.Helper() if reflect.DeepEqual(want, got) { - t.Fatalf("want %#v, got %#v", want, got) + t.Fatalf("did not want %#v, got %#v", want, got) } } diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index dcca6288c8..759845a9e5 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -97,6 +97,12 @@ func (w *GzipResponseWriter) Write(b []byte) (int, error) { // If a Content-Type wasn't specified, infer it from the current buffer. if ct == "" { ct = http.DetectContentType(w.buf) + } + + // Handles the intended case of setting a nil Content-Type (as for http/server or http/fs) + // Set the header only if the key does not exist + _, haveType := w.Header()["Content-Type"] + if !haveType { w.Header().Set(contentType, ct) } // If the Content-Type is acceptable to GZIP, initialize the GZIP writer. diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go index ca5ab9f909..e33e8fa887 100644 --- a/gzhttp/gzip_test.go +++ b/gzhttp/gzip_test.go @@ -649,3 +649,18 @@ func newTestHandler(body string) http.Handler { } })) } + +func TestGzipHandlerNilContentType(t *testing.T) { + // This just exists to provide something for GzipHandler to wrap. + handler := newTestHandler(testBody) + + // content-type header not set when provided nil + + req, _ := http.NewRequest("GET", "/whatever", nil) + req.Header.Set("Accept-Encoding", "gzip") + res := httptest.NewRecorder() + res.Header()["Content-Type"] = nil + handler.ServeHTTP(res, req) + + assertEqual(t, "", res.Header().Get("Content-Type")) +} From 0d3b2880d326d79c2fb36d542721386459766257 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 31 May 2021 13:43:35 +0200 Subject: [PATCH 03/14] Implement a variant of https://github.com/nytimes/gziphandler/pull/81 --- gzhttp/gzip.go | 76 +++++++++++++++++++++++++++++++++++---------- gzhttp/gzip_test.go | 23 ++++++++++++++ 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index 759845a9e5..6dd54c51be 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -54,7 +54,7 @@ type GzipResponseWriter struct { buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. ignore bool // If true, then we immediately passthru writes to the underlying ResponseWriter. - contentTypes []parsedContentType // Only compress if the response is one of these content-types. All are accepted if empty. + contentTypeFilter func(ct string) bool // Only compress if the response is one of these content-types. All are accepted if empty. } type GzipResponseWriterWithCloseNotify struct { @@ -87,7 +87,7 @@ func (w *GzipResponseWriter) Write(b []byte) (int, error) { ce = w.Header().Get(contentEncoding) ) // Only continue if they didn't already choose an encoding or a known unhandled content length or type. - if ce == "" && (cl == 0 || cl >= w.minSize) && (ct == "" || handleContentType(w.contentTypes, ct)) { + if ce == "" && (cl == 0 || cl >= w.minSize) && (ct == "" || w.contentTypeFilter(ct)) { // If the current buffer is less than minSize and a Content-Length isn't set, then wait until we have more data. if len(w.buf) < w.minSize && cl == 0 { return len(b), nil @@ -106,7 +106,7 @@ func (w *GzipResponseWriter) Write(b []byte) (int, error) { w.Header().Set(contentType, ct) } // If the Content-Type is acceptable to GZIP, initialize the GZIP writer. - if handleContentType(w.contentTypes, ct) { + if w.contentTypeFilter(ct) { if err := w.startGzip(); err != nil { return 0, err } @@ -273,6 +273,9 @@ func NewGzipHandler(opts ...option) (func(http.Handler) http.Handler, error) { Levels: gzkp.Levels, New: gzkp.NewWriter, }, + contentTypes: func(ct string) bool { + return true + }, } for _, o := range opts { @@ -288,11 +291,11 @@ func NewGzipHandler(opts ...option) (func(http.Handler) http.Handler, error) { w.Header().Add(vary, acceptEncoding) if acceptsGzip(r) { gw := &GzipResponseWriter{ - ResponseWriter: w, - gwFactory: c.writer, - level: c.level, - minSize: c.minSize, - contentTypes: c.contentTypes, + ResponseWriter: w, + gwFactory: c.writer, + level: c.level, + minSize: c.minSize, + contentTypeFilter: c.contentTypes, } defer gw.Close() @@ -344,7 +347,7 @@ type config struct { minSize int level int writer writer.GzipWriterFactory - contentTypes []parsedContentType + contentTypes func(ct string) bool } func (c *config) validate() error { @@ -403,31 +406,72 @@ func Implementation(writer writer.GzipWriterFactory) option { // // By default, responses are gzipped regardless of // Content-Type. +// +// Setting this will override any previous Content Type settings. func ContentTypes(types []string) option { return func(c *config) { - c.contentTypes = []parsedContentType{} + var contentTypes []parsedContentType for _, v := range types { mediaType, params, err := mime.ParseMediaType(v) if err == nil { - c.contentTypes = append(c.contentTypes, parsedContentType{mediaType, params}) + contentTypes = append(contentTypes, parsedContentType{mediaType, params}) } } + c.contentTypes = func(ct string) bool { + return handleContentType(contentTypes, ct) + } } } -/* -func ContentTypeFilter(func(contentType string) bool) { +// ExceptContentTypes specifies a list of content types to compare +// the Content-Type header to before compressing. If none +// match, the response will be compressed. +// +// Content types are compared in a case-insensitive, whitespace-ignored +// manner. +// +// A MIME type without any other directive will match a content type +// that has the same MIME type, regardless of that content type's other +// directives. I.e., "text/html" will match both "text/html" and +// "text/html; charset=utf-8". +// +// A MIME type with any other directive will only match a content type +// that has the same MIME type and other directives. I.e., +// "text/html; charset=utf-8" will only match "text/html; charset=utf-8". +// +// By default, responses are gzipped regardless of +// Content-Type. +// +// Setting this will override any previous Content Type settings. +func ExceptContentTypes(types []string) option { return func(c *config) { - c.contentTypes = []parsedContentType{} + var contentTypes []parsedContentType for _, v := range types { mediaType, params, err := mime.ParseMediaType(v) if err == nil { - c.contentTypes = append(c.contentTypes, parsedContentType{mediaType, params}) + contentTypes = append(contentTypes, parsedContentType{mediaType, params}) } } + c.contentTypes = func(ct string) bool { + return !handleContentType(contentTypes, ct) + } + } +} + +// ContentTypeFilter allows adding a custom content type filter. +// +// The supplied function must return true/false to indicate if content +// should be compressed. +// +// When called no parsing of the content type 'ct' has been done. +// It may have been set or auto-detected. +// +// Setting this will override any previous Content Type settings. +func ContentTypeFilter(compress func(ct string) bool) option { + return func(c *config) { + c.contentTypes = compress } } -*/ // GzipHandler wraps an HTTP handler, to transparently gzip the response body if // the client supports it (via the Accept-Encoding header). This will compress at diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go index e33e8fa887..a894bf5409 100644 --- a/gzhttp/gzip_test.go +++ b/gzhttp/gzip_test.go @@ -579,6 +579,29 @@ func TestContentTypes(t *testing.T) { assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding")) } }) + t.Run("not-"+tt.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", tt.contentType) + io.WriteString(w, testBody) + }) + + wrapper, err := NewGzipHandler(ExceptContentTypes(tt.acceptedContentTypes)) + assertNil(t, err) + + req, _ := http.NewRequest("GET", "/whatever", nil) + req.Header.Set("Accept-Encoding", "gzip") + resp := httptest.NewRecorder() + wrapper(handler).ServeHTTP(resp, req) + res := resp.Result() + + assertEqual(t, 200, res.StatusCode) + if !tt.expectedGzip { + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + } else { + assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding")) + } + }) } } From a40438b065cb34aa8dba43cd601cac68b848f72b Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 31 May 2021 13:46:34 +0200 Subject: [PATCH 04/14] Fix https://github.com/nytimes/gziphandler/issues/103 --- gzhttp/gzip.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index 6dd54c51be..d837430a09 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -170,13 +170,14 @@ func (w *GzipResponseWriter) startPlain() error { return nil } n, err := w.ResponseWriter.Write(w.buf) - w.buf = nil // This should never happen (per io.Writer docs), but if the write didn't // accept the entire buffer but returned no specific error, we have no clue // what's going on, so abort just to be safe. if err == nil && n < len(w.buf) { err = io.ErrShortWrite } + + w.buf = nil return err } From c09cc20c5908b8f44dd8f464338481060fd7c2a6 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 31 May 2021 15:52:59 +0200 Subject: [PATCH 05/14] Clean up, add more docs. --- gzhttp/README.md | 48 ++++++++++--- gzhttp/gzip.go | 80 ++++++++++++--------- gzhttp/gzip_test.go | 170 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 235 insertions(+), 63 deletions(-) diff --git a/gzhttp/README.md b/gzhttp/README.md index 2816371274..984c5c9976 100644 --- a/gzhttp/README.md +++ b/gzhttp/README.md @@ -4,7 +4,7 @@ Gzip Handler This is a tiny Go package which wraps HTTP handlers to transparently gzip the response body, for clients which support it. -This package is forked from the [dead nytimes/gziphandler](https://github.com/nytimes/gziphandler) +This package is forked from the dead [nytimes/gziphandler](https://github.com/nytimes/gziphandler) and extends functionality for it. ## Install @@ -12,9 +12,14 @@ and extends functionality for it. go get -u github.com/klauspost/compress ``` +## Documentation + +[![Go Reference](https://pkg.go.dev/badge/github.com/klauspost/compress/gzhttp.svg)](https://pkg.go.dev/github.com/klauspost/compress/gzhttp) + + ## Usage -Call `GzipHandler` with any handler (an object which implements the +For the simplest usage call `MustGzipHandler` with any handler (an object which implements the `http.Handler` interface), and it'll return a new handler which gzips the response. For example: @@ -33,22 +38,47 @@ func main() { io.WriteString(w, "Hello, World") }) - http.Handle("/", gzhttp.GzipHandler(handler)) + http.Handle("/", gzhttp.MustGzipHandler(handler)) http.ListenAndServe("0.0.0.0:8000", nil) } ``` +This will wrap a handler using the default options. -## Documentation +To specify custom options a reusable wrapper can be created that can be used to wrap +any number of handlers. -The docs can be found at [godoc.org][], as usual. +```Go +package main +import ( + "io" + "log" + "net/http" + + "github.com/klauspost/compress/gzhttp" + "github.com/klauspost/compress/gzip" +) -## License +func main() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, "Hello, World") + }) + + // Create a reusable wrapper with custom options. + wrapper, err := gzhttp.NewWrapper(gzhttp.MinSize(2000), gzhttp.CompressionLevel(gzip.BestSpeed)) + if err != nil { + log.Fatalln(err) + } + + http.Handle("/", wrapper(handler)) + http.ListenAndServe("0.0.0.0:8000", nil) +} +``` -[Apache 2.0][license]. +## License +[Apache 2.0](LICENSE) -[docs]: https://godoc.org/github.com/NYTimes/gziphandler -[license]: https://github.com/NYTimes/gziphandler/blob/master/LICENSE diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index d837430a09..04dfb53979 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "sync" "github.com/klauspost/compress/gzhttp/writer" "github.com/klauspost/compress/gzhttp/writer/gzkp" @@ -250,23 +251,24 @@ func (w *GzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { // verify Hijacker interface implementation var _ http.Hijacker = &GzipResponseWriter{} -// MustNewGzipLevelHandler behaves just like NewGzipLevelHandler except that in -// an error case it panics rather than returning an error. -func MustNewGzipLevelHandler(level int) func(http.Handler) http.Handler { - wrap, err := NewGzipHandler(CompressionLevel(level)) - if err != nil { - panic(err) - } - return wrap -} +var onceDefault sync.Once +var defaultWrapper func(http.Handler) http.Handler + +// MustGzipHandler allows to easily wrap an http handler with default settings. +func MustGzipHandler(h http.Handler) http.Handler { + onceDefault.Do(func() { + var err error + defaultWrapper, err = NewWrapper() + if err != nil { + panic(err) + } + }) -// NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller -// specify the minimum size before compression. -func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) { - return NewGzipHandler(CompressionLevel(level), MinSize(minSize)) + return defaultWrapper(h) } -func NewGzipHandler(opts ...option) (func(http.Handler) http.Handler, error) { +// NewWrapper returns a reusable wrapper with the supplied options. +func NewWrapper(opts ...option) (func(http.Handler) http.Handler, error) { c := &config{ level: gzip.DefaultCompression, minSize: DefaultMinSize, @@ -274,9 +276,7 @@ func NewGzipHandler(opts ...option) (func(http.Handler) http.Handler, error) { Levels: gzkp.Levels, New: gzkp.NewWriter, }, - contentTypes: func(ct string) bool { - return true - }, + contentTypes: DefaultContentTypeFilter, } for _, o := range opts { @@ -405,10 +405,9 @@ func Implementation(writer writer.GzipWriterFactory) option { // that has the same MIME type and other directives. I.e., // "text/html; charset=utf-8" will only match "text/html; charset=utf-8". // -// By default, responses are gzipped regardless of -// Content-Type. +// By default common compressed audio, video and archive formats, see DefaultContentTypeFilter. // -// Setting this will override any previous Content Type settings. +// Setting this will override default and any previous Content Type settings. func ContentTypes(types []string) option { return func(c *config) { var contentTypes []parsedContentType @@ -440,10 +439,9 @@ func ContentTypes(types []string) option { // that has the same MIME type and other directives. I.e., // "text/html; charset=utf-8" will only match "text/html; charset=utf-8". // -// By default, responses are gzipped regardless of -// Content-Type. +// By default common compressed audio, video and archive formats, see DefaultContentTypeFilter. // -// Setting this will override any previous Content Type settings. +// Setting this will override default and any previous Content Type settings. func ExceptContentTypes(types []string) option { return func(c *config) { var contentTypes []parsedContentType @@ -467,21 +465,13 @@ func ExceptContentTypes(types []string) option { // When called no parsing of the content type 'ct' has been done. // It may have been set or auto-detected. // -// Setting this will override any previous Content Type settings. +// Setting this will override default and any previous Content Type settings. func ContentTypeFilter(compress func(ct string) bool) option { return func(c *config) { c.contentTypes = compress } } -// GzipHandler wraps an HTTP handler, to transparently gzip the response body if -// the client supports it (via the Accept-Encoding header). This will compress at -// the default compression level. -func GzipHandler(h http.Handler, opts ...option) http.Handler { - wrapper, _ := NewGzipHandler(opts...) - return wrapper(h) -} - // acceptsGzip returns true if the given HTTP request indicates that it will // accept a gzipped response. func acceptsGzip(r *http.Request) bool { @@ -568,3 +558,29 @@ func parseCoding(s string) (coding string, qvalue float64, err error) { return } + +// Don't compress any audio/video types. +var excludePrefixDefault = []string{"video/", "audio/"} + +// Skip a bunch of compressed types that contains this string. +var excludeContainsDefault = []string{"compress", "zip"} + +// DefaultContentTypeFilter excludes common compressed audio, video and archive formats. +func DefaultContentTypeFilter(ct string) bool { + ct = strings.TrimSpace(strings.ToLower(ct)) + if ct == "" { + return true + } + for _, s := range excludeContainsDefault { + if strings.Contains(ct, s) { + return false + } + } + + for _, prefix := range excludePrefixDefault { + if strings.HasPrefix(ct, prefix) { + return false + } + } + return true +} diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go index a894bf5409..7057ce0e1a 100644 --- a/gzhttp/gzip_test.go +++ b/gzhttp/gzip_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "io/ioutil" + "log" "net" "net/http" "net/http/httptest" @@ -41,7 +42,7 @@ func TestParseEncodings(t *testing.T) { } } -func TestGzipHandler(t *testing.T) { +func TestMustNewGzipHandler(t *testing.T) { // This just exists to provide something for GzipHandler to wrap. handler := newTestHandler(testBody) @@ -117,7 +118,7 @@ func TestNewGzipLevelHandler(t *testing.T) { for lvl := gzip.StatelessCompression; lvl <= gzip.BestCompression; lvl++ { t.Run(fmt.Sprint(lvl), func(t *testing.T) { - wrapper, err := NewGzipHandler(CompressionLevel(lvl)) + wrapper, err := NewWrapper(CompressionLevel(lvl)) assertNil(t, err) req, _ := http.NewRequest("GET", "/whatever", nil) @@ -138,21 +139,21 @@ func TestNewGzipLevelHandler(t *testing.T) { func TestNewGzipLevelHandlerReturnsErrorForInvalidLevels(t *testing.T) { var err error - _, err = NewGzipHandler(CompressionLevel(-42)) + _, err = NewWrapper(CompressionLevel(-42)) assertNotNil(t, err) - _, err = NewGzipHandler(CompressionLevel(42)) + _, err = NewWrapper(CompressionLevel(42)) assertNotNil(t, err) } func TestMustNewGzipLevelHandlerWillPanic(t *testing.T) { defer func() { - if r := recover(); r == nil { - t.Error("panic was not called") + if r := recover(); r != nil { + t.Error("panic was called with", r) } }() - _ = MustNewGzipLevelHandler(-42) + _ = MustGzipHandler(nil) } func TestGzipHandlerNoBody(t *testing.T) { @@ -172,7 +173,7 @@ func TestGzipHandlerNoBody(t *testing.T) { for num, test := range tests { t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) { - handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.statusCode) if test.body != nil { w.Write(test.body) @@ -234,7 +235,7 @@ func TestGzipHandlerContentLength(t *testing.T) { for num, test := range tests { t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) { - srv.Handler = GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv.Handler = MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if test.bodyLen > 0 { w.Header().Set("Content-Length", strconv.Itoa(test.bodyLen)) } @@ -277,7 +278,7 @@ func TestGzipHandlerContentLength(t *testing.T) { } func TestGzipHandlerMinSizeMustBePositive(t *testing.T) { - _, err := NewGzipLevelAndMinSize(gzip.DefaultCompression, -1) + _, err := NewWrapper(MinSize(-1)) assertNotNil(t, err) } @@ -285,7 +286,7 @@ func TestGzipHandlerMinSize(t *testing.T) { responseLength := 0 b := []byte{'x'} - wrapper, _ := NewGzipLevelAndMinSize(gzip.DefaultCompression, 128) + wrapper, _ := NewWrapper(MinSize(128)) handler := wrapper(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { // Write responses one byte at a time to ensure that the flush @@ -332,7 +333,7 @@ func (w *panicOnSecondWriteHeaderWriter) WriteHeader(s int) { } func TestGzipHandlerDoubleWriteHeader(t *testing.T) { - handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", "15000") // Specifically write the header here w.WriteHeader(304) @@ -371,7 +372,7 @@ func TestGzipHandlerDoubleWriteHeader(t *testing.T) { } func TestStatusCodes(t *testing.T) { - handler := GzipHandler(http.NotFoundHandler()) + handler := MustGzipHandler(http.NotFoundHandler()) r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Accept-Encoding", "gzip") w := httptest.NewRecorder() @@ -385,7 +386,7 @@ func TestStatusCodes(t *testing.T) { func TestFlushBeforeWrite(t *testing.T) { b := []byte(testBody) - handler := GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + handler := MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotFound) rw.(http.Flusher).Flush() rw.Write(b) @@ -404,7 +405,7 @@ func TestFlushBeforeWrite(t *testing.T) { func TestImplementCloseNotifier(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(acceptEncoding, "gzip") - GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { _, ok := rw.(http.CloseNotifier) // response writer must implement http.CloseNotifier assertEqual(t, true, ok) @@ -414,7 +415,7 @@ func TestImplementCloseNotifier(t *testing.T) { func TestImplementFlusherAndCloseNotifier(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(acceptEncoding, "gzip") - GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { _, okCloseNotifier := rw.(http.CloseNotifier) // response writer must implement http.CloseNotifier assertEqual(t, true, okCloseNotifier) @@ -427,7 +428,7 @@ func TestImplementFlusherAndCloseNotifier(t *testing.T) { func TestNotImplementCloseNotifier(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(acceptEncoding, "gzip") - GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { _, ok := rw.(http.CloseNotifier) // response writer must not implement http.CloseNotifier assertEqual(t, false, ok) @@ -453,7 +454,7 @@ func (m *mockRWCloseNotify) WriteHeader(int) { } func TestIgnoreSubsequentWriteHeader(t *testing.T) { - handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) w.WriteHeader(404) })) @@ -473,7 +474,7 @@ func TestDontWriteWhenNotWrittenTo(t *testing.T) { // ensure the gzip middleware doesn't touch the actual ResponseWriter // either. - handler0 := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler0 := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) handler1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -563,7 +564,7 @@ func TestContentTypes(t *testing.T) { io.WriteString(w, testBody) }) - wrapper, err := NewGzipHandler(ContentTypes(tt.acceptedContentTypes)) + wrapper, err := NewWrapper(ContentTypes(tt.acceptedContentTypes)) assertNil(t, err) req, _ := http.NewRequest("GET", "/whatever", nil) @@ -586,7 +587,7 @@ func TestContentTypes(t *testing.T) { io.WriteString(w, testBody) }) - wrapper, err := NewGzipHandler(ExceptContentTypes(tt.acceptedContentTypes)) + wrapper, err := NewWrapper(ExceptContentTypes(tt.acceptedContentTypes)) assertNil(t, err) req, _ := http.NewRequest("GET", "/whatever", nil) @@ -605,6 +606,92 @@ func TestContentTypes(t *testing.T) { } } +var contentTypeTest2 = []struct { + name string + contentType string + expectedGzip bool +}{ + { + name: "Always gzip when content types are empty", + contentType: "", + expectedGzip: true, + }, + { + name: "MIME match", + contentType: "application/json", + expectedGzip: true, + }, + { + name: "MIME no match", + contentType: "text/xml", + expectedGzip: true, + }, + + { + name: "MIME match case insensitive", + contentType: "Video/Something", + expectedGzip: false, + }, + { + name: "MIME match case insensitive", + contentType: "audio/Something", + expectedGzip: false, + }, + { + name: "MIME match ignore whitespace", + contentType: " video/mp4", + expectedGzip: false, + }, + { + name: "without prefix..", + contentType: "avideo/mp4", + expectedGzip: true, + }, + { + name: "application/zip", + contentType: "application/zip;lalala", + expectedGzip: false, + }, + { + name: "x-zip-compressed", + contentType: "application/x-zip-compressed", + expectedGzip: false, + }, + { + name: "application/x-gzip", + contentType: "application/x-gzip", + expectedGzip: false, + }, +} + +func TestDefaultContentTypes(t *testing.T) { + for _, tt := range contentTypeTest2 { + t.Run(tt.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", tt.contentType) + io.WriteString(w, testBody) + }) + + wrapper, err := NewWrapper() + assertNil(t, err) + + req, _ := http.NewRequest("GET", "/whatever", nil) + req.Header.Set("Accept-Encoding", "gzip") + resp := httptest.NewRecorder() + wrapper(handler).ServeHTTP(resp, req) + res := resp.Result() + + assertEqual(t, 200, res.StatusCode) + if tt.expectedGzip { + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + } else { + assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding")) + } + }) + } +} + // -------------------------------------------------------------------- func BenchmarkGzipHandler_S2k(b *testing.B) { benchmark(b, false, 2048) } @@ -662,7 +749,7 @@ func runBenchmark(b *testing.B, req *http.Request, handler http.Handler) { } func newTestHandler(body string) http.Handler { - return GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/gzipped": w.Header().Set("Content-Encoding", "gzip") @@ -687,3 +774,42 @@ func TestGzipHandlerNilContentType(t *testing.T) { assertEqual(t, "", res.Header().Get("Content-Type")) } + +func ExampleNewWrapper() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, "Hello, World, Welcome to the jungle...") + }) + handler2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "Hello, Another World.................") + }) + + // Create a reusable wrapper with custom options. + wrapper, err := NewWrapper(MinSize(20), CompressionLevel(gzip.BestSpeed)) + if err != nil { + log.Fatalln(err) + } + server := http.NewServeMux() + server.Handle("/a", wrapper(handler)) + server.Handle("/b", wrapper(handler2)) + + test := httptest.NewServer(server) + defer test.Close() + + resp, err := http.Get(test.URL + "/a") + if err != nil { + log.Fatalln(err) + } + content, _ := ioutil.ReadAll(resp.Body) + fmt.Println(string(content)) + + resp, err = http.Get(test.URL + "/b") + if err != nil { + log.Fatalln(err) + } + content, _ = ioutil.ReadAll(resp.Body) + fmt.Println(string(content)) + // Output: + // Hello, World, Welcome to the jungle... + // Hello, Another World................. +} From 06ce31928a587423d5cc0f08587fe95fb8ea1d47 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 31 May 2021 16:39:02 +0200 Subject: [PATCH 06/14] Simplify q parsing for less allocs. --- gzhttp/gzip.go | 34 ++++++++++++++++++++++++---------- gzhttp/gzip_test.go | 9 +++++++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index 04dfb53979..f41bc4b524 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -475,8 +475,7 @@ func ContentTypeFilter(compress func(ct string) bool) option { // acceptsGzip returns true if the given HTTP request indicates that it will // accept a gzipped response. func acceptsGzip(r *http.Request) bool { - acceptedEncodings, _ := parseEncodings(r.Header.Get(acceptEncoding)) - return acceptedEncodings["gzip"] > 0.0 + return parseEncodingGzip(r.Header.Get(acceptEncoding)) > 0 } // returns true if we've been configured to compress the specific content type. @@ -500,13 +499,28 @@ func handleContentType(contentTypes []parsedContentType, ct string) bool { return false } -// parseEncodings attempts to parse a list of codings, per RFC 2616, as might -// appear in an Accept-Encoding header. It returns a map of content-codings to -// quality values, and an error containing the errors encountered. It's probably -// safe to ignore those, because silently ignoring errors is how the internet -// works. -// -// See: http://tools.ietf.org/html/rfc2616#section-14.3. +// parseEncodingGzip returns the qvalue of gzip compression. +func parseEncodingGzip(s string) float64 { + s = strings.TrimSpace(s) + + for len(s) > 0 { + stop := strings.IndexByte(s, ',') + if stop < 0 { + stop = len(s) + } + coding, qvalue, _ := parseCoding(s[:stop]) + + if coding == "gzip" { + return qvalue + } + if stop == len(s) { + break + } + s = s[stop+1:] + } + return 0 +} + func parseEncodings(s string) (codings, error) { split := strings.Split(s, ",") c := make(codings, len(split)) @@ -531,7 +545,7 @@ func parseEncodings(s string) (codings, error) { return c, nil } -// parseCoding parses a single conding (content-coding with an optional qvalue), +// parseCoding parses a single coding (content-coding with an optional qvalue), // as might appear in an Accept-Encoding header. It attempts to forgive minor // formatting errors. func parseCoding(s string) (coding string, qvalue float64, err error) { diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go index 7057ce0e1a..29da856f2d 100644 --- a/gzhttp/gzip_test.go +++ b/gzhttp/gzip_test.go @@ -26,6 +26,7 @@ func TestParseEncodings(t *testing.T) { // Examples from RFC 2616 "compress, gzip": {"compress": 1.0, "gzip": 1.0}, + ",,,,": {}, "": {}, "*": {"*": 1.0}, "compress;q=0.5, gzip;q=1.0": {"compress": 0.5, "gzip": 1.0}, @@ -37,8 +38,12 @@ func TestParseEncodings(t *testing.T) { } for eg, exp := range examples { - act, _ := parseEncodings(eg) - assertEqual(t, exp, act) + t.Run(eg, func(t *testing.T) { + act, _ := parseEncodings(eg) + assertEqual(t, exp, act) + gz := parseEncodingGzip(eg) + assertEqual(t, exp["gzip"], gz) + }) } } From f51d6b0694703ae067b6847e4a64975ba29fca7a Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Mon, 31 May 2021 17:16:05 +0200 Subject: [PATCH 07/14] Update docs. --- gzhttp/README.md | 55 +++++++++++++++++++++++++++++++++++++++++++-- gzhttp/gzip.go | 9 ++++++-- gzhttp/gzip_test.go | 24 ++++++++++---------- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/gzhttp/README.md b/gzhttp/README.md index 984c5c9976..133d3566b1 100644 --- a/gzhttp/README.md +++ b/gzhttp/README.md @@ -19,7 +19,7 @@ go get -u github.com/klauspost/compress ## Usage -For the simplest usage call `MustGzipHandler` with any handler (an object which implements the +For the simplest usage call `GzipHandler` with any handler (an object which implements the `http.Handler` interface), and it'll return a new handler which gzips the response. For example: @@ -38,7 +38,7 @@ func main() { io.WriteString(w, "Hello, World") }) - http.Handle("/", gzhttp.MustGzipHandler(handler)) + http.Handle("/", gzhttp.GzipHandler(handler)) http.ListenAndServe("0.0.0.0:8000", nil) } ``` @@ -75,8 +75,59 @@ func main() { http.Handle("/", wrapper(handler)) http.ListenAndServe("0.0.0.0:8000", nil) } + ``` +## Stateless compression + +In cases where you expect to run many thousands of compressors concurrently, +but with very little activity you can use stateless compression. +This is not intended for regular web servers serving individual requests. + +Use `CompressionLevel(-3)` or `CompressionLevel(gzip.StatelessCompression)` to enable. + +See [more details on stateless compression](https://github.com/klauspost/compress#stateless-compression). + +## Migrating from gziphandler + +This package removes some of the extra constructors. +When replacing, this can be used to find a replacement. + +* `GzipHandler(h)` -> `GzipHandler(h)` (keep as-is) +* `GzipHandlerWithOpts(opts...)` -> `NewWrapper(opts...)` +* `MustNewGzipLevelHandler(n)` -> `NewWrapper(CompressionLevel(n))` +* `NewGzipLevelAndMinSize(n, s)` -> `NewWrapper(CompressionLevel(n), MinSize(s))` + +# Performance + +Speed compared to [nytimes/gziphandler](https://github.com/nytimes/gziphandler) with default settings: + +``` +λ benchcmp before.txt after.txt +benchmark old ns/op new ns/op delta +BenchmarkGzipHandler_S2k-32 51302 25554 -50.19% +BenchmarkGzipHandler_S20k-32 301426 174900 -41.98% +BenchmarkGzipHandler_S100k-32 1546203 912349 -40.99% +BenchmarkGzipHandler_P2k-32 3973 2116 -46.74% +BenchmarkGzipHandler_P20k-32 20319 12237 -39.78% +BenchmarkGzipHandler_P100k-32 96079 57348 -40.31% + +benchmark old MB/s new MB/s speedup +BenchmarkGzipHandler_S2k-32 39.92 80.14 2.01x +BenchmarkGzipHandler_S20k-32 67.94 117.10 1.72x +BenchmarkGzipHandler_S100k-32 66.23 112.24 1.69x +BenchmarkGzipHandler_P2k-32 515.44 967.76 1.88x +BenchmarkGzipHandler_P20k-32 1007.92 1673.55 1.66x +BenchmarkGzipHandler_P100k-32 1065.79 1785.58 1.68x + +benchmark old allocs new allocs delta +BenchmarkGzipHandler_S2k-32 22 19 -13.64% +BenchmarkGzipHandler_S20k-32 25 22 -12.00% +BenchmarkGzipHandler_S100k-32 28 25 -10.71% +BenchmarkGzipHandler_P2k-32 22 19 -13.64% +BenchmarkGzipHandler_P20k-32 25 22 -12.00% +BenchmarkGzipHandler_P100k-32 27 24 -11.11% +``` ## License [Apache 2.0](LICENSE) diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index f41bc4b524..2a5664a3f4 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -254,8 +254,8 @@ var _ http.Hijacker = &GzipResponseWriter{} var onceDefault sync.Once var defaultWrapper func(http.Handler) http.Handler -// MustGzipHandler allows to easily wrap an http handler with default settings. -func MustGzipHandler(h http.Handler) http.Handler { +// GzipHandler allows to easily wrap an http handler with default settings. +func GzipHandler(h http.Handler) http.Handler { onceDefault.Do(func() { var err error defaultWrapper, err = NewWrapper() @@ -598,3 +598,8 @@ func DefaultContentTypeFilter(ct string) bool { } return true } + +// CompressAllContentTypeFilter will compress all mime types. +func CompressAllContentTypeFilter(ct string) bool { + return true +} diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go index 29da856f2d..d59986a4d4 100644 --- a/gzhttp/gzip_test.go +++ b/gzhttp/gzip_test.go @@ -158,7 +158,7 @@ func TestMustNewGzipLevelHandlerWillPanic(t *testing.T) { } }() - _ = MustGzipHandler(nil) + _ = GzipHandler(nil) } func TestGzipHandlerNoBody(t *testing.T) { @@ -178,7 +178,7 @@ func TestGzipHandlerNoBody(t *testing.T) { for num, test := range tests { t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) { - handler := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.statusCode) if test.body != nil { w.Write(test.body) @@ -240,7 +240,7 @@ func TestGzipHandlerContentLength(t *testing.T) { for num, test := range tests { t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) { - srv.Handler = MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv.Handler = GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if test.bodyLen > 0 { w.Header().Set("Content-Length", strconv.Itoa(test.bodyLen)) } @@ -338,7 +338,7 @@ func (w *panicOnSecondWriteHeaderWriter) WriteHeader(s int) { } func TestGzipHandlerDoubleWriteHeader(t *testing.T) { - handler := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", "15000") // Specifically write the header here w.WriteHeader(304) @@ -377,7 +377,7 @@ func TestGzipHandlerDoubleWriteHeader(t *testing.T) { } func TestStatusCodes(t *testing.T) { - handler := MustGzipHandler(http.NotFoundHandler()) + handler := GzipHandler(http.NotFoundHandler()) r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Accept-Encoding", "gzip") w := httptest.NewRecorder() @@ -391,7 +391,7 @@ func TestStatusCodes(t *testing.T) { func TestFlushBeforeWrite(t *testing.T) { b := []byte(testBody) - handler := MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + handler := GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotFound) rw.(http.Flusher).Flush() rw.Write(b) @@ -410,7 +410,7 @@ func TestFlushBeforeWrite(t *testing.T) { func TestImplementCloseNotifier(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(acceptEncoding, "gzip") - MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { _, ok := rw.(http.CloseNotifier) // response writer must implement http.CloseNotifier assertEqual(t, true, ok) @@ -420,7 +420,7 @@ func TestImplementCloseNotifier(t *testing.T) { func TestImplementFlusherAndCloseNotifier(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(acceptEncoding, "gzip") - MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { _, okCloseNotifier := rw.(http.CloseNotifier) // response writer must implement http.CloseNotifier assertEqual(t, true, okCloseNotifier) @@ -433,7 +433,7 @@ func TestImplementFlusherAndCloseNotifier(t *testing.T) { func TestNotImplementCloseNotifier(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(acceptEncoding, "gzip") - MustGzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { _, ok := rw.(http.CloseNotifier) // response writer must not implement http.CloseNotifier assertEqual(t, false, ok) @@ -459,7 +459,7 @@ func (m *mockRWCloseNotify) WriteHeader(int) { } func TestIgnoreSubsequentWriteHeader(t *testing.T) { - handler := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) w.WriteHeader(404) })) @@ -479,7 +479,7 @@ func TestDontWriteWhenNotWrittenTo(t *testing.T) { // ensure the gzip middleware doesn't touch the actual ResponseWriter // either. - handler0 := MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler0 := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) handler1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -754,7 +754,7 @@ func runBenchmark(b *testing.B, req *http.Request, handler http.Handler) { } func newTestHandler(body string) http.Handler { - return MustGzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/gzipped": w.Header().Set("Content-Encoding", "gzip") From 99c164cbc52eb458843bad49516228e0a4b7119b Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Tue, 1 Jun 2021 10:23:14 +0200 Subject: [PATCH 08/14] Rename package --- gzhttp/writer/{stdlib => gzstd}/stdlib.go | 2 +- gzhttp/writer/{stdlib => gzstd}/stdlib_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename gzhttp/writer/{stdlib => gzstd}/stdlib.go (99%) rename gzhttp/writer/{stdlib => gzstd}/stdlib_test.go (97%) diff --git a/gzhttp/writer/stdlib/stdlib.go b/gzhttp/writer/gzstd/stdlib.go similarity index 99% rename from gzhttp/writer/stdlib/stdlib.go rename to gzhttp/writer/gzstd/stdlib.go index f646eba297..0e7abd9117 100644 --- a/gzhttp/writer/stdlib/stdlib.go +++ b/gzhttp/writer/gzstd/stdlib.go @@ -1,4 +1,4 @@ -package gzkp +package gzstd import ( "compress/gzip" diff --git a/gzhttp/writer/stdlib/stdlib_test.go b/gzhttp/writer/gzstd/stdlib_test.go similarity index 97% rename from gzhttp/writer/stdlib/stdlib_test.go rename to gzhttp/writer/gzstd/stdlib_test.go index 6782f97bf6..d43feda977 100644 --- a/gzhttp/writer/stdlib/stdlib_test.go +++ b/gzhttp/writer/gzstd/stdlib_test.go @@ -1,4 +1,4 @@ -package gzkp +package gzstd import ( "bytes" From 0c26ad7229e3b10734c6d829c8da93597c3dfe73 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Tue, 1 Jun 2021 11:33:39 +0200 Subject: [PATCH 09/14] Docs... --- gzhttp/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gzhttp/README.md b/gzhttp/README.md index 133d3566b1..e5c8b5fbbe 100644 --- a/gzhttp/README.md +++ b/gzhttp/README.md @@ -85,6 +85,7 @@ but with very little activity you can use stateless compression. This is not intended for regular web servers serving individual requests. Use `CompressionLevel(-3)` or `CompressionLevel(gzip.StatelessCompression)` to enable. +Consider adding a [`bufio.Writer`](https://golang.org/pkg/bufio/#NewWriterSize) with a small buffer. See [more details on stateless compression](https://github.com/klauspost/compress#stateless-compression). @@ -98,6 +99,9 @@ When replacing, this can be used to find a replacement. * `MustNewGzipLevelHandler(n)` -> `NewWrapper(CompressionLevel(n))` * `NewGzipLevelAndMinSize(n, s)` -> `NewWrapper(CompressionLevel(n), MinSize(s))` +By default, some mime types will now be excluded. +To re-enable compression of all types, use the `ContentTypeFilter(gzhttp.CompressAllContentTypeFilter)` option. + # Performance Speed compared to [nytimes/gziphandler](https://github.com/nytimes/gziphandler) with default settings: From 8125fd74ba3c2effb11e7de50d4c75e0a6fdbca3 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Tue, 1 Jun 2021 15:10:38 +0200 Subject: [PATCH 10/14] Add Transport wrapper --- gzhttp/README.md | 105 +- gzhttp/asserts_test.go | 6 +- gzhttp/examples_test.go | 74 + gzhttp/gzip.go | 2 +- gzhttp/gzip_test.go | 42 +- gzhttp/testdata/benchmark.json | 5456 ++++++++++++++++++++++++++++++++ gzhttp/transport.go | 116 + gzhttp/transport_test.go | 174 + 8 files changed, 5907 insertions(+), 68 deletions(-) create mode 100644 gzhttp/examples_test.go create mode 100644 gzhttp/testdata/benchmark.json create mode 100644 gzhttp/transport.go create mode 100644 gzhttp/transport_test.go diff --git a/gzhttp/README.md b/gzhttp/README.md index e5c8b5fbbe..7e39449c91 100644 --- a/gzhttp/README.md +++ b/gzhttp/README.md @@ -1,9 +1,12 @@ Gzip Handler ============ -This is a tiny Go package which wraps HTTP handlers to transparently gzip the +This is a tiny Go package which wraps HTTP server handlers to transparently gzip the response body, for clients which support it. +For HTTP clients we provide a transport wrapper that will do gzip decompression +faster than what the standard library offers. + This package is forked from the dead [nytimes/gziphandler](https://github.com/nytimes/gziphandler) and extends functionality for it. @@ -19,6 +22,56 @@ go get -u github.com/klauspost/compress ## Usage +There are 2 main parts, one for http servers and one for http clients. + +### Client + +The standard library automatically adds gzip compression to most requests +and handles decompression of the responses. + +However, by wrapping the transport we are able to override this and provide +our own (faster) decompressor. + +Wrapping is done on the Transport of the http client: + +```Go +func ExampleTransport() { + // Get an HTTP client. + client := http.Client{ + // Wrap the transport: + Transport: gzhttp.Transport(http.DefaultTransport), + } + + resp, err := client.Get("https://google.com") + if err != nil { + return + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + fmt.Println("body:", string(body)) +} +``` + +Speed compared to standard library for an approximate 127KB payload: + +``` +BenchmarkTransport + +Single core: +BenchmarkTransport/gzhttp-32 1995 609791 ns/op 214.14 MB/s 10129 B/op 73 allocs/op +BenchmarkTransport/stdlib-32 1567 772161 ns/op 169.11 MB/s 53950 B/op 99 allocs/op + +Multi Core: +BenchmarkTransport/gzhttp-par-32 8428 199682 ns/op 653.95 MB/s 42332 B/op 120 allocs/op +BenchmarkTransport/stdlib-par-32 3946 311886 ns/op 418.69 MB/s 71972 B/op 137 allocs/op +``` + +This includes both serving the http request, parsing requests and decompressing. +For this payload size decompression is actually only about 40% of CPU load on the multi-core benchmark. + +### Server + For the simplest usage call `GzipHandler` with any handler (an object which implements the `http.Handler` interface), and it'll return a new handler which gzips the response. For example: @@ -78,31 +131,8 @@ func main() { ``` -## Stateless compression -In cases where you expect to run many thousands of compressors concurrently, -but with very little activity you can use stateless compression. -This is not intended for regular web servers serving individual requests. - -Use `CompressionLevel(-3)` or `CompressionLevel(gzip.StatelessCompression)` to enable. -Consider adding a [`bufio.Writer`](https://golang.org/pkg/bufio/#NewWriterSize) with a small buffer. - -See [more details on stateless compression](https://github.com/klauspost/compress#stateless-compression). - -## Migrating from gziphandler - -This package removes some of the extra constructors. -When replacing, this can be used to find a replacement. - -* `GzipHandler(h)` -> `GzipHandler(h)` (keep as-is) -* `GzipHandlerWithOpts(opts...)` -> `NewWrapper(opts...)` -* `MustNewGzipLevelHandler(n)` -> `NewWrapper(CompressionLevel(n))` -* `NewGzipLevelAndMinSize(n, s)` -> `NewWrapper(CompressionLevel(n), MinSize(s))` - -By default, some mime types will now be excluded. -To re-enable compression of all types, use the `ContentTypeFilter(gzhttp.CompressAllContentTypeFilter)` option. - -# Performance +### Performance Speed compared to [nytimes/gziphandler](https://github.com/nytimes/gziphandler) with default settings: @@ -132,6 +162,31 @@ BenchmarkGzipHandler_P2k-32 22 19 -13.64% BenchmarkGzipHandler_P20k-32 25 22 -12.00% BenchmarkGzipHandler_P100k-32 27 24 -11.11% ``` + +### Stateless compression + +In cases where you expect to run many thousands of compressors concurrently, +but with very little activity you can use stateless compression. +This is not intended for regular web servers serving individual requests. + +Use `CompressionLevel(-3)` or `CompressionLevel(gzip.StatelessCompression)` to enable. +Consider adding a [`bufio.Writer`](https://golang.org/pkg/bufio/#NewWriterSize) with a small buffer. + +See [more details on stateless compression](https://github.com/klauspost/compress#stateless-compression). + +### Migrating from gziphandler + +This package removes some of the extra constructors. +When replacing, this can be used to find a replacement. + +* `GzipHandler(h)` -> `GzipHandler(h)` (keep as-is) +* `GzipHandlerWithOpts(opts...)` -> `NewWrapper(opts...)` +* `MustNewGzipLevelHandler(n)` -> `NewWrapper(CompressionLevel(n))` +* `NewGzipLevelAndMinSize(n, s)` -> `NewWrapper(CompressionLevel(n), MinSize(s))` + +By default, some mime types will now be excluded. +To re-enable compression of all types, use the `ContentTypeFilter(gzhttp.CompressAllContentTypeFilter)` option. + ## License [Apache 2.0](LICENSE) diff --git a/gzhttp/asserts_test.go b/gzhttp/asserts_test.go index 5bbfd80227..1c454f3a82 100644 --- a/gzhttp/asserts_test.go +++ b/gzhttp/asserts_test.go @@ -1,4 +1,8 @@ -package gziphandler +// Copyright (c) 2021 Klaus Post. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gzhttp import ( "reflect" diff --git a/gzhttp/examples_test.go b/gzhttp/examples_test.go new file mode 100644 index 0000000000..888e6dbbca --- /dev/null +++ b/gzhttp/examples_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2021 Klaus Post. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gzhttp_test + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + + "github.com/klauspost/compress/gzhttp" + "github.com/klauspost/compress/gzip" +) + +func ExampleTransport() { + // Get a client. + client := http.Client{ + // Wrap the transport: + Transport: gzhttp.Transport(http.DefaultTransport), + } + + resp, err := client.Get("https://google.com") + if err != nil { + fmt.Println(err) + return + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + fmt.Println("body:", string(body)) +} + +func ExampleNewWrapper() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, "Hello, World, Welcome to the jungle...") + }) + handler2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "Hello, Another World.................") + }) + + // Create a reusable wrapper with custom options. + wrapper, err := gzhttp.NewWrapper(gzhttp.MinSize(20), gzhttp.CompressionLevel(gzip.BestSpeed)) + if err != nil { + log.Fatalln(err) + } + server := http.NewServeMux() + server.Handle("/a", wrapper(handler)) + server.Handle("/b", wrapper(handler2)) + + test := httptest.NewServer(server) + defer test.Close() + + resp, err := http.Get(test.URL + "/a") + if err != nil { + log.Fatalln(err) + } + content, _ := ioutil.ReadAll(resp.Body) + fmt.Println(string(content)) + + resp, err = http.Get(test.URL + "/b") + if err != nil { + log.Fatalln(err) + } + content, _ = ioutil.ReadAll(resp.Body) + fmt.Println(string(content)) + // Output: + // Hello, World, Welcome to the jungle... + // Hello, Another World................. +} diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index 2a5664a3f4..0142a6f531 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -1,4 +1,4 @@ -package gziphandler +package gzhttp import ( "bufio" diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go index d59986a4d4..10075c1242 100644 --- a/gzhttp/gzip_test.go +++ b/gzhttp/gzip_test.go @@ -1,11 +1,10 @@ -package gziphandler +package gzhttp import ( "bytes" "fmt" "io" "io/ioutil" - "log" "net" "net/http" "net/http/httptest" @@ -779,42 +778,3 @@ func TestGzipHandlerNilContentType(t *testing.T) { assertEqual(t, "", res.Header().Get("Content-Type")) } - -func ExampleNewWrapper() { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - io.WriteString(w, "Hello, World, Welcome to the jungle...") - }) - handler2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, "Hello, Another World.................") - }) - - // Create a reusable wrapper with custom options. - wrapper, err := NewWrapper(MinSize(20), CompressionLevel(gzip.BestSpeed)) - if err != nil { - log.Fatalln(err) - } - server := http.NewServeMux() - server.Handle("/a", wrapper(handler)) - server.Handle("/b", wrapper(handler2)) - - test := httptest.NewServer(server) - defer test.Close() - - resp, err := http.Get(test.URL + "/a") - if err != nil { - log.Fatalln(err) - } - content, _ := ioutil.ReadAll(resp.Body) - fmt.Println(string(content)) - - resp, err = http.Get(test.URL + "/b") - if err != nil { - log.Fatalln(err) - } - content, _ = ioutil.ReadAll(resp.Body) - fmt.Println(string(content)) - // Output: - // Hello, World, Welcome to the jungle... - // Hello, Another World................. -} diff --git a/gzhttp/testdata/benchmark.json b/gzhttp/testdata/benchmark.json new file mode 100644 index 0000000000..d9180d6ace --- /dev/null +++ b/gzhttp/testdata/benchmark.json @@ -0,0 +1,5456 @@ +[ + { + "_id": "55d2fc86da7c3f96f90aa005", + "age": 20, + "name": "Luann Grant", + "gender": "female", + "company": "ZOGAK", + "email": "luanngrant@zogak.com", + "phone": "+1 (915) 479-2908" + }, + { + "_id": "55d2fc8653953bc9a0958a92", + "age": 34, + "name": "Sanders Gonzalez", + "gender": "male", + "company": "PIVITOL", + "email": "sandersgonzalez@pivitol.com", + "phone": "+1 (914) 563-2007" + }, + { + "_id": "55d2fc86a38634b0954fe3c0", + "age": 26, + "name": "Compton Terry", + "gender": "male", + "company": "KOZGENE", + "email": "comptonterry@kozgene.com", + "phone": "+1 (812) 558-2536" + }, + { + "_id": "55d2fc86edf1be88253e4e2e", + "age": 29, + "name": "Erma Armstrong", + "gender": "female", + "company": "DYNO", + "email": "ermaarmstrong@dyno.com", + "phone": "+1 (811) 556-3980" + }, + { + "_id": "55d2fc86bee1bf1f233f8170", + "age": 20, + "name": "Carson Garcia", + "gender": "male", + "company": "JUNIPOOR", + "email": "carsongarcia@junipoor.com", + "phone": "+1 (820) 410-3221" + }, + { + "_id": "55d2fc864810db4a3738dea8", + "age": 38, + "name": "Henson Townsend", + "gender": "male", + "company": "OVIUM", + "email": "hensontownsend@ovium.com", + "phone": "+1 (982) 412-3108" + }, + { + "_id": "55d2fc86d714d77af8ed3fe4", + "age": 34, + "name": "Yesenia Garner", + "gender": "female", + "company": "TERRAGO", + "email": "yeseniagarner@terrago.com", + "phone": "+1 (878) 561-2314" + }, + { + "_id": "55d2fc867651311b2e11925a", + "age": 31, + "name": "Rachelle Stanton", + "gender": "female", + "company": "UNISURE", + "email": "rachellestanton@unisure.com", + "phone": "+1 (961) 504-3072" + }, + { + "_id": "55d2fc866415f6c03d5228eb", + "age": 30, + "name": "Franklin Rasmussen", + "gender": "male", + "company": "CAPSCREEN", + "email": "franklinrasmussen@capscreen.com", + "phone": "+1 (886) 525-2217" + }, + { + "_id": "55d2fc86e2e0fa5f81fe5279", + "age": 32, + "name": "Fischer Humphrey", + "gender": "male", + "company": "ATGEN", + "email": "fischerhumphrey@atgen.com", + "phone": "+1 (855) 424-3693" + }, + { + "_id": "55d2fc862c2a416777837c76", + "age": 29, + "name": "Olsen Moran", + "gender": "male", + "company": "SULTRAX", + "email": "olsenmoran@sultrax.com", + "phone": "+1 (860) 583-3380" + }, + { + "_id": "55d2fc868c3c9e44d59ec2a2", + "age": 29, + "name": "Mattie Myers", + "gender": "female", + "company": "VALPREAL", + "email": "mattiemyers@valpreal.com", + "phone": "+1 (834) 587-2707" + }, + { + "_id": "55d2fc860a6afc8beebe477d", + "age": 39, + "name": "Cleveland Jordan", + "gender": "male", + "company": "XLEEN", + "email": "clevelandjordan@xleen.com", + "phone": "+1 (848) 449-2037" + }, + { + "_id": "55d2fc8605766af1b531b5ea", + "age": 35, + "name": "Gordon Rios", + "gender": "male", + "company": "BULLZONE", + "email": "gordonrios@bullzone.com", + "phone": "+1 (882) 436-2216" + }, + { + "_id": "55d2fc86e052bfb7cf9b5a38", + "age": 33, + "name": "Todd Burch", + "gender": "male", + "company": "MOTOVATE", + "email": "toddburch@motovate.com", + "phone": "+1 (911) 470-2129" + }, + { + "_id": "55d2fc86212924928b4112d6", + "age": 31, + "name": "Autumn Strong", + "gender": "female", + "company": "NURPLEX", + "email": "autumnstrong@nurplex.com", + "phone": "+1 (827) 483-2571" + }, + { + "_id": "55d2fc86286c9bc87359e326", + "age": 27, + "name": "Rochelle Fitzgerald", + "gender": "female", + "company": "RAMEON", + "email": "rochellefitzgerald@rameon.com", + "phone": "+1 (820) 402-3411" + }, + { + "_id": "55d2fc86511439244d21c569", + "age": 24, + "name": "Perry Hopkins", + "gender": "male", + "company": "KRAGGLE", + "email": "perryhopkins@kraggle.com", + "phone": "+1 (826) 469-3928" + }, + { + "_id": "55d2fc868b8652b54c66051c", + "age": 37, + "name": "Guzman Williamson", + "gender": "male", + "company": "OCTOCORE", + "email": "guzmanwilliamson@octocore.com", + "phone": "+1 (826) 529-3380" + }, + { + "_id": "55d2fc86f16a5a4c310483df", + "age": 28, + "name": "Sheri Thompson", + "gender": "female", + "company": "AUTOGRATE", + "email": "sherithompson@autograte.com", + "phone": "+1 (942) 463-2727" + }, + { + "_id": "55d2fc86675d3040fc35daa8", + "age": 24, + "name": "Walton Macdonald", + "gender": "male", + "company": "ZIZZLE", + "email": "waltonmacdonald@zizzle.com", + "phone": "+1 (990) 510-2656" + }, + { + "_id": "55d2fc864e90e236f9e5a174", + "age": 39, + "name": "Gwendolyn Ross", + "gender": "female", + "company": "XIIX", + "email": "gwendolynross@xiix.com", + "phone": "+1 (869) 565-2774" + }, + { + "_id": "55d2fc866f306c575a36cb97", + "age": 23, + "name": "Sexton Herring", + "gender": "male", + "company": "MEDIOT", + "email": "sextonherring@mediot.com", + "phone": "+1 (935) 510-2049" + }, + { + "_id": "55d2fc867526db78550711a3", + "age": 26, + "name": "Twila Vang", + "gender": "female", + "company": "ARCTIQ", + "email": "twilavang@arctiq.com", + "phone": "+1 (979) 429-2135" + }, + { + "_id": "55d2fc86fe5e24c1b4b0dc96", + "age": 26, + "name": "Marjorie Snider", + "gender": "female", + "company": "QUARX", + "email": "marjoriesnider@quarx.com", + "phone": "+1 (913) 469-2916" + }, + { + "_id": "55d2fc861e85e0104fa7d33e", + "age": 36, + "name": "Malone Diaz", + "gender": "male", + "company": "COMVEYOR", + "email": "malonediaz@comveyor.com", + "phone": "+1 (882) 541-3306" + }, + { + "_id": "55d2fc86bc04a4fa0a338403", + "age": 39, + "name": "Amelia Sanford", + "gender": "female", + "company": "IPLAX", + "email": "ameliasanford@iplax.com", + "phone": "+1 (872) 589-3509" + }, + { + "_id": "55d2fc86f7854f672e80c1dd", + "age": 27, + "name": "Kristie Fernandez", + "gender": "female", + "company": "RONBERT", + "email": "kristiefernandez@ronbert.com", + "phone": "+1 (983) 419-3564" + }, + { + "_id": "55d2fc867faa140f152b7229", + "age": 23, + "name": "Bray Wyatt", + "gender": "male", + "company": "DEMINIMUM", + "email": "braywyatt@deminimum.com", + "phone": "+1 (943) 485-3961" + }, + { + "_id": "55d2fc863a42779d68c0dd53", + "age": 36, + "name": "Meyer Pickett", + "gender": "male", + "company": "GENEKOM", + "email": "meyerpickett@genekom.com", + "phone": "+1 (999) 534-3038" + }, + { + "_id": "55d2fc86095f5ceb3c57efeb", + "age": 22, + "name": "Carlson Ramirez", + "gender": "male", + "company": "KANGLE", + "email": "carlsonramirez@kangle.com", + "phone": "+1 (972) 590-3152" + }, + { + "_id": "55d2fc868036a76e42f30954", + "age": 24, + "name": "Roth Murray", + "gender": "male", + "company": "DEEPENDS", + "email": "rothmurray@deepends.com", + "phone": "+1 (944) 408-2208" + }, + { + "_id": "55d2fc8688ac278604ea592b", + "age": 24, + "name": "Cecelia Lambert", + "gender": "female", + "company": "SLOFAST", + "email": "cecelialambert@slofast.com", + "phone": "+1 (907) 485-2284" + }, + { + "_id": "55d2fc86b05fc3906ce838a2", + "age": 31, + "name": "Leah Ferguson", + "gender": "female", + "company": "NEBULEAN", + "email": "leahferguson@nebulean.com", + "phone": "+1 (883) 574-2101" + }, + { + "_id": "55d2fc86d21f9d73f11076b9", + "age": 28, + "name": "Therese Mueller", + "gender": "female", + "company": "STEELFAB", + "email": "theresemueller@steelfab.com", + "phone": "+1 (920) 485-2265" + }, + { + "_id": "55d2fc866254f87d0389dc98", + "age": 32, + "name": "Wanda Byrd", + "gender": "female", + "company": "HOTCAKES", + "email": "wandabyrd@hotcakes.com", + "phone": "+1 (893) 579-3658" + }, + { + "_id": "55d2fc868cf04450063aba0e", + "age": 25, + "name": "Felecia Mccall", + "gender": "female", + "company": "GINK", + "email": "feleciamccall@gink.com", + "phone": "+1 (848) 486-3047" + }, + { + "_id": "55d2fc86e0ee0341850d35ab", + "age": 37, + "name": "Goldie Stafford", + "gender": "female", + "company": "FIREWAX", + "email": "goldiestafford@firewax.com", + "phone": "+1 (831) 507-3578" + }, + { + "_id": "55d2fc868acfa20489a61720", + "age": 20, + "name": "Amy Patterson", + "gender": "female", + "company": "KNEEDLES", + "email": "amypatterson@kneedles.com", + "phone": "+1 (950) 478-3558" + }, + { + "_id": "55d2fc8680c104681b074336", + "age": 29, + "name": "Robles Alford", + "gender": "male", + "company": "TALENDULA", + "email": "roblesalford@talendula.com", + "phone": "+1 (813) 424-3650" + }, + { + "_id": "55d2fc86faeb57024907ea70", + "age": 28, + "name": "Adkins Hampton", + "gender": "male", + "company": "SYNKGEN", + "email": "adkinshampton@synkgen.com", + "phone": "+1 (974) 522-2517" + }, + { + "_id": "55d2fc866ba878e18ee65c0f", + "age": 32, + "name": "Bernadine Trevino", + "gender": "female", + "company": "EVENTEX", + "email": "bernadinetrevino@eventex.com", + "phone": "+1 (914) 577-2655" + }, + { + "_id": "55d2fc86524364ec8cb2d0c7", + "age": 38, + "name": "Robin Le", + "gender": "female", + "company": "GALLAXIA", + "email": "robinle@gallaxia.com", + "phone": "+1 (821) 472-2416" + }, + { + "_id": "55d2fc86c53dc98dacabc399", + "age": 23, + "name": "Leanna Hicks", + "gender": "female", + "company": "ZEROLOGY", + "email": "leannahicks@zerology.com", + "phone": "+1 (946) 531-3368" + }, + { + "_id": "55d2fc86e293b8cfb2d1a5bd", + "age": 31, + "name": "Herman Bridges", + "gender": "male", + "company": "DADABASE", + "email": "hermanbridges@dadabase.com", + "phone": "+1 (836) 408-3169" + }, + { + "_id": "55d2fc865dfc0cc61ec50b41", + "age": 30, + "name": "Olive Terrell", + "gender": "female", + "company": "ACCRUEX", + "email": "oliveterrell@accruex.com", + "phone": "+1 (837) 573-2059" + }, + { + "_id": "55d2fc8669dc78cdeb374ff5", + "age": 26, + "name": "Miranda Banks", + "gender": "female", + "company": "GEOFARM", + "email": "mirandabanks@geofarm.com", + "phone": "+1 (984) 574-2877" + }, + { + "_id": "55d2fc86f471f565df37a756", + "age": 31, + "name": "Noelle Wolf", + "gender": "female", + "company": "ENDIPINE", + "email": "noellewolf@endipine.com", + "phone": "+1 (880) 548-2427" + }, + { + "_id": "55d2fc8603e133136e5ca325", + "age": 38, + "name": "Amber Klein", + "gender": "female", + "company": "ISOPLEX", + "email": "amberklein@isoplex.com", + "phone": "+1 (853) 548-3856" + }, + { + "_id": "55d2fc86962b00cfd86e9d8d", + "age": 32, + "name": "Jodie Mcclain", + "gender": "female", + "company": "BESTO", + "email": "jodiemcclain@besto.com", + "phone": "+1 (857) 417-2691" + }, + { + "_id": "55d2fc8622eaf8052986a5bb", + "age": 35, + "name": "Margo Melendez", + "gender": "female", + "company": "KLUGGER", + "email": "margomelendez@klugger.com", + "phone": "+1 (823) 445-3570" + }, + { + "_id": "55d2fc861e37cd298741e801", + "age": 37, + "name": "Castaneda Dudley", + "gender": "male", + "company": "URBANSHEE", + "email": "castanedadudley@urbanshee.com", + "phone": "+1 (865) 449-2445" + }, + { + "_id": "55d2fc86e4c07ff933f7b531", + "age": 40, + "name": "Sherman Combs", + "gender": "male", + "company": "GRACKER", + "email": "shermancombs@gracker.com", + "phone": "+1 (817) 472-2316" + }, + { + "_id": "55d2fc86c4613e0a622727e6", + "age": 31, + "name": "Louise Nichols", + "gender": "female", + "company": "SPORTAN", + "email": "louisenichols@sportan.com", + "phone": "+1 (823) 543-2230" + }, + { + "_id": "55d2fc86d40dd218587cc632", + "age": 29, + "name": "Daniels Jacobs", + "gender": "male", + "company": "CALCULA", + "email": "danielsjacobs@calcula.com", + "phone": "+1 (979) 448-2244" + }, + { + "_id": "55d2fc86b44f3ec927f3b2e1", + "age": 22, + "name": "Cristina Crosby", + "gender": "female", + "company": "PHUEL", + "email": "cristinacrosby@phuel.com", + "phone": "+1 (816) 504-3557" + }, + { + "_id": "55d2fc86ba635f5436325487", + "age": 23, + "name": "Tisha Sawyer", + "gender": "female", + "company": "IMAGEFLOW", + "email": "tishasawyer@imageflow.com", + "phone": "+1 (894) 551-2933" + }, + { + "_id": "55d2fc8640e8af981a47216a", + "age": 32, + "name": "Janice Graham", + "gender": "female", + "company": "PYRAMIS", + "email": "janicegraham@pyramis.com", + "phone": "+1 (986) 409-2529" + }, + { + "_id": "55d2fc86cdbe80c5cb184731", + "age": 22, + "name": "Holman Joyce", + "gender": "male", + "company": "VERTON", + "email": "holmanjoyce@verton.com", + "phone": "+1 (955) 445-2054" + }, + { + "_id": "55d2fc860f70e0d0890e8e47", + "age": 21, + "name": "Webb Sears", + "gender": "male", + "company": "ENQUILITY", + "email": "webbsears@enquility.com", + "phone": "+1 (961) 406-3600" + }, + { + "_id": "55d2fc86150c8385661fde6f", + "age": 29, + "name": "Bush Farrell", + "gender": "male", + "company": "ENDICIL", + "email": "bushfarrell@endicil.com", + "phone": "+1 (894) 550-2963" + }, + { + "_id": "55d2fc862e2f1ca9662b03e3", + "age": 36, + "name": "Gay Walters", + "gender": "male", + "company": "ZENSURE", + "email": "gaywalters@zensure.com", + "phone": "+1 (859) 467-3816" + }, + { + "_id": "55d2fc86da1249ca6d3a068f", + "age": 33, + "name": "Marquez Palmer", + "gender": "male", + "company": "TURNLING", + "email": "marquezpalmer@turnling.com", + "phone": "+1 (878) 522-3859" + }, + { + "_id": "55d2fc86a61a66a69db5c8a9", + "age": 29, + "name": "Huber Parrish", + "gender": "male", + "company": "DELPHIDE", + "email": "huberparrish@delphide.com", + "phone": "+1 (947) 406-3267" + }, + { + "_id": "55d2fc86fff10fda8f106c2a", + "age": 22, + "name": "Foreman Cohen", + "gender": "male", + "company": "TROPOLIS", + "email": "foremancohen@tropolis.com", + "phone": "+1 (802) 409-2459" + }, + { + "_id": "55d2fc8632b185024494802d", + "age": 31, + "name": "Kathryn Peterson", + "gender": "female", + "company": "PROSURE", + "email": "kathrynpeterson@prosure.com", + "phone": "+1 (811) 531-3085" + }, + { + "_id": "55d2fc86f8e11410bb5b8e88", + "age": 34, + "name": "Aisha Duke", + "gender": "female", + "company": "APPLICA", + "email": "aishaduke@applica.com", + "phone": "+1 (977) 438-2754" + }, + { + "_id": "55d2fc86190a0097728af887", + "age": 37, + "name": "Maynard Henderson", + "gender": "male", + "company": "ILLUMITY", + "email": "maynardhenderson@illumity.com", + "phone": "+1 (832) 472-2261" + }, + { + "_id": "55d2fc86d298e6e1cf9332df", + "age": 33, + "name": "Villarreal Santiago", + "gender": "male", + "company": "QUAILCOM", + "email": "villarrealsantiago@quailcom.com", + "phone": "+1 (981) 422-2572" + }, + { + "_id": "55d2fc86b7eb3b9ed01fc186", + "age": 30, + "name": "Diaz Allison", + "gender": "male", + "company": "ISOSTREAM", + "email": "diazallison@isostream.com", + "phone": "+1 (883) 530-2186" + }, + { + "_id": "55d2fc86da92ac006d69c236", + "age": 20, + "name": "Kirkland Mccullough", + "gender": "male", + "company": "ADORNICA", + "email": "kirklandmccullough@adornica.com", + "phone": "+1 (950) 556-3562" + }, + { + "_id": "55d2fc86195f59929397f2fc", + "age": 32, + "name": "Dolores Howard", + "gender": "female", + "company": "ZINCA", + "email": "doloreshoward@zinca.com", + "phone": "+1 (904) 450-2101" + }, + { + "_id": "55d2fc860c9f707c22eaca7c", + "age": 26, + "name": "Mills Gamble", + "gender": "male", + "company": "BLEEKO", + "email": "millsgamble@bleeko.com", + "phone": "+1 (942) 402-2630" + }, + { + "_id": "55d2fc86b539183079fcab65", + "age": 35, + "name": "Walls Dotson", + "gender": "male", + "company": "GRUPOLI", + "email": "wallsdotson@grupoli.com", + "phone": "+1 (979) 477-3065" + }, + { + "_id": "55d2fc866377b14a737261c3", + "age": 29, + "name": "Hannah Coleman", + "gender": "female", + "company": "COMCUR", + "email": "hannahcoleman@comcur.com", + "phone": "+1 (944) 595-2415" + }, + { + "_id": "55d2fc867b77f0e8d5f08276", + "age": 25, + "name": "Kinney Oneill", + "gender": "male", + "company": "ELPRO", + "email": "kinneyoneill@elpro.com", + "phone": "+1 (851) 570-2363" + }, + { + "_id": "55d2fc868745f5f71578624c", + "age": 32, + "name": "Bennett Henson", + "gender": "male", + "company": "ZISIS", + "email": "bennetthenson@zisis.com", + "phone": "+1 (958) 584-2257" + }, + { + "_id": "55d2fc8604d1e7576b1a83fb", + "age": 32, + "name": "Ross Chavez", + "gender": "male", + "company": "DUOFLEX", + "email": "rosschavez@duoflex.com", + "phone": "+1 (890) 556-2052" + }, + { + "_id": "55d2fc86bd556093136802dd", + "age": 26, + "name": "Raymond Rutledge", + "gender": "male", + "company": "BUNGA", + "email": "raymondrutledge@bunga.com", + "phone": "+1 (994) 504-2118" + }, + { + "_id": "55d2fc86a122c7d89560c130", + "age": 31, + "name": "Rosemary Larsen", + "gender": "female", + "company": "EXOPLODE", + "email": "rosemarylarsen@exoplode.com", + "phone": "+1 (864) 558-3569" + }, + { + "_id": "55d2fc86dd3fe9ea62fdbea1", + "age": 27, + "name": "Tara Roth", + "gender": "female", + "company": "GAPTEC", + "email": "tararoth@gaptec.com", + "phone": "+1 (893) 531-2962" + }, + { + "_id": "55d2fc866f372ca411cd425d", + "age": 30, + "name": "Ronda Sheppard", + "gender": "female", + "company": "GREEKER", + "email": "rondasheppard@greeker.com", + "phone": "+1 (963) 569-2851" + }, + { + "_id": "55d2fc860a0dc369ac8bed9f", + "age": 37, + "name": "Nita Washington", + "gender": "female", + "company": "AQUASURE", + "email": "nitawashington@aquasure.com", + "phone": "+1 (858) 579-3734" + }, + { + "_id": "55d2fc863d59ecccd8b482b7", + "age": 34, + "name": "Shannon Sanchez", + "gender": "male", + "company": "QUORDATE", + "email": "shannonsanchez@quordate.com", + "phone": "+1 (880) 542-2259" + }, + { + "_id": "55d2fc861c73e366d8983f98", + "age": 23, + "name": "Amalia George", + "gender": "female", + "company": "QUINEX", + "email": "amaliageorge@quinex.com", + "phone": "+1 (918) 412-3805" + }, + { + "_id": "55d2fc866910c4329a167746", + "age": 30, + "name": "West Parsons", + "gender": "male", + "company": "RODEMCO", + "email": "westparsons@rodemco.com", + "phone": "+1 (869) 480-3404" + }, + { + "_id": "55d2fc861d97ff31a8eb7f06", + "age": 30, + "name": "Day Mercado", + "gender": "male", + "company": "ENJOLA", + "email": "daymercado@enjola.com", + "phone": "+1 (861) 546-2601" + }, + { + "_id": "55d2fc861e100baa15994161", + "age": 20, + "name": "Hubbard Randolph", + "gender": "male", + "company": "INSOURCE", + "email": "hubbardrandolph@insource.com", + "phone": "+1 (939) 422-2753" + }, + { + "_id": "55d2fc86dc33a0101292ea57", + "age": 32, + "name": "Ward Patrick", + "gender": "male", + "company": "ZUVY", + "email": "wardpatrick@zuvy.com", + "phone": "+1 (852) 461-3079" + }, + { + "_id": "55d2fc863074ff55ec22d7ac", + "age": 26, + "name": "Barker Small", + "gender": "male", + "company": "ZIDANT", + "email": "barkersmall@zidant.com", + "phone": "+1 (901) 514-3653" + }, + { + "_id": "55d2fc86008067de21a65447", + "age": 26, + "name": "Margret Porter", + "gender": "female", + "company": "DYMI", + "email": "margretporter@dymi.com", + "phone": "+1 (820) 404-2199" + }, + { + "_id": "55d2fc8694b7f8670d10781e", + "age": 40, + "name": "Roslyn Richmond", + "gender": "female", + "company": "SONGBIRD", + "email": "roslynrichmond@songbird.com", + "phone": "+1 (913) 406-3720" + }, + { + "_id": "55d2fc86ffff28aef88e1d69", + "age": 29, + "name": "Jimenez Yates", + "gender": "male", + "company": "IDETICA", + "email": "jimenezyates@idetica.com", + "phone": "+1 (803) 421-2358" + }, + { + "_id": "55d2fc8604bfeab3a975b2d8", + "age": 21, + "name": "Mcneil Jensen", + "gender": "male", + "company": "EXOBLUE", + "email": "mcneiljensen@exoblue.com", + "phone": "+1 (856) 581-3756" + }, + { + "_id": "55d2fc86bee368958f567c45", + "age": 40, + "name": "Millicent Trujillo", + "gender": "female", + "company": "ARTWORLDS", + "email": "millicenttrujillo@artworlds.com", + "phone": "+1 (926) 456-2237" + }, + { + "_id": "55d2fc86de2a5a6babe51d48", + "age": 26, + "name": "Dorothea Duffy", + "gender": "female", + "company": "EBIDCO", + "email": "dorotheaduffy@ebidco.com", + "phone": "+1 (873) 458-2694" + }, + { + "_id": "55d2fc86f8793320727d2020", + "age": 34, + "name": "Gloria Ward", + "gender": "female", + "company": "EXOVENT", + "email": "gloriaward@exovent.com", + "phone": "+1 (948) 552-3275" + }, + { + "_id": "55d2fc86967e5599adde68e8", + "age": 33, + "name": "Denise Hogan", + "gender": "female", + "company": "GLEAMINK", + "email": "denisehogan@gleamink.com", + "phone": "+1 (902) 544-2943" + }, + { + "_id": "55d2fc862833baf2918f7860", + "age": 23, + "name": "Clemons Berger", + "gender": "male", + "company": "ARTIQ", + "email": "clemonsberger@artiq.com", + "phone": "+1 (873) 516-2440" + }, + { + "_id": "55d2fc8610af4c20d426854a", + "age": 40, + "name": "Colette Scott", + "gender": "female", + "company": "INVENTURE", + "email": "colettescott@inventure.com", + "phone": "+1 (814) 556-3466" + }, + { + "_id": "55d2fc8697af4d0d06a2c4a6", + "age": 34, + "name": "Chavez Aguilar", + "gender": "male", + "company": "ISBOL", + "email": "chavezaguilar@isbol.com", + "phone": "+1 (952) 543-3992" + }, + { + "_id": "55d2fc861da12d86036c3e48", + "age": 29, + "name": "Arnold Collier", + "gender": "male", + "company": "MELBACOR", + "email": "arnoldcollier@melbacor.com", + "phone": "+1 (907) 404-2090" + }, + { + "_id": "55d2fc86deb921c78b56fec1", + "age": 24, + "name": "Nixon Ingram", + "gender": "male", + "company": "ELENTRIX", + "email": "nixoningram@elentrix.com", + "phone": "+1 (884) 596-3023" + }, + { + "_id": "55d2fc86a8b288ba6b8aa48f", + "age": 28, + "name": "Hurst Hull", + "gender": "male", + "company": "INCUBUS", + "email": "hursthull@incubus.com", + "phone": "+1 (970) 445-2279" + }, + { + "_id": "55d2fc864dc593d113336b1b", + "age": 32, + "name": "Giles Jacobson", + "gender": "male", + "company": "MIXERS", + "email": "gilesjacobson@mixers.com", + "phone": "+1 (868) 402-3161" + }, + { + "_id": "55d2fc86fd0246fcd0c8f864", + "age": 25, + "name": "Millie Giles", + "gender": "female", + "company": "ZAJ", + "email": "milliegiles@zaj.com", + "phone": "+1 (831) 535-3535" + }, + { + "_id": "55d2fc86dceeca0deb9f6d67", + "age": 21, + "name": "Sargent Morse", + "gender": "male", + "company": "ANARCO", + "email": "sargentmorse@anarco.com", + "phone": "+1 (994) 536-2563" + }, + { + "_id": "55d2fc86abbd1add64df6aa2", + "age": 26, + "name": "Reed Camacho", + "gender": "male", + "company": "ICOLOGY", + "email": "reedcamacho@icology.com", + "phone": "+1 (999) 408-3042" + }, + { + "_id": "55d2fc86a2593db6d584f022", + "age": 21, + "name": "Bryant Colon", + "gender": "male", + "company": "ZANILLA", + "email": "bryantcolon@zanilla.com", + "phone": "+1 (964) 570-3418" + }, + { + "_id": "55d2fc86f980f74ba093fde6", + "age": 38, + "name": "Rosella Pierce", + "gender": "female", + "company": "PRIMORDIA", + "email": "rosellapierce@primordia.com", + "phone": "+1 (936) 579-3014" + }, + { + "_id": "55d2fc86797a0001329071fb", + "age": 20, + "name": "Patrice Mckee", + "gender": "female", + "company": "GEEKY", + "email": "patricemckee@geeky.com", + "phone": "+1 (866) 565-3764" + }, + { + "_id": "55d2fc86bcc6181d0b8cae17", + "age": 29, + "name": "Wilkinson Levy", + "gender": "male", + "company": "FISHLAND", + "email": "wilkinsonlevy@fishland.com", + "phone": "+1 (998) 509-3800" + }, + { + "_id": "55d2fc86784b1e22004d637d", + "age": 28, + "name": "Isabella Snyder", + "gender": "female", + "company": "TETRATREX", + "email": "isabellasnyder@tetratrex.com", + "phone": "+1 (998) 451-2871" + }, + { + "_id": "55d2fc8603abab523ff1f02d", + "age": 35, + "name": "Claudine Berg", + "gender": "female", + "company": "ZYTREK", + "email": "claudineberg@zytrek.com", + "phone": "+1 (836) 563-3376" + }, + { + "_id": "55d2fc862796e94846a82a8b", + "age": 27, + "name": "Hyde Simon", + "gender": "male", + "company": "FUELWORKS", + "email": "hydesimon@fuelworks.com", + "phone": "+1 (872) 594-2291" + }, + { + "_id": "55d2fc86f2fa84a4f7ca4548", + "age": 29, + "name": "Wendy Roberts", + "gender": "female", + "company": "OPTICALL", + "email": "wendyroberts@opticall.com", + "phone": "+1 (895) 534-3852" + }, + { + "_id": "55d2fc8651dbb76674b6d2df", + "age": 40, + "name": "Tasha Erickson", + "gender": "female", + "company": "PURIA", + "email": "tashaerickson@puria.com", + "phone": "+1 (905) 526-2175" + }, + { + "_id": "55d2fc86b7f867e76e709d68", + "age": 32, + "name": "Marisa Dunlap", + "gender": "female", + "company": "GLUKGLUK", + "email": "marisadunlap@glukgluk.com", + "phone": "+1 (855) 400-2200" + }, + { + "_id": "55d2fc8695b6ccc0707570af", + "age": 38, + "name": "Kaitlin Baldwin", + "gender": "female", + "company": "EGYPTO", + "email": "kaitlinbaldwin@egypto.com", + "phone": "+1 (980) 482-3256" + }, + { + "_id": "55d2fc86f36b539d4494395d", + "age": 38, + "name": "Abigail Kirkland", + "gender": "female", + "company": "INRT", + "email": "abigailkirkland@inrt.com", + "phone": "+1 (916) 440-3469" + }, + { + "_id": "55d2fc86e401343dbf1de380", + "age": 34, + "name": "Marci Maynard", + "gender": "female", + "company": "INSURITY", + "email": "marcimaynard@insurity.com", + "phone": "+1 (917) 482-3601" + }, + { + "_id": "55d2fc86ba3f7fdb738899f4", + "age": 31, + "name": "Tanya Michael", + "gender": "female", + "company": "ENVIRE", + "email": "tanyamichael@envire.com", + "phone": "+1 (826) 436-3177" + }, + { + "_id": "55d2fc861cb8a1d3151ae2c1", + "age": 34, + "name": "Farrell Irwin", + "gender": "male", + "company": "EXTRAGEN", + "email": "farrellirwin@extragen.com", + "phone": "+1 (948) 442-3796" + }, + { + "_id": "55d2fc86b90f948949af3d8e", + "age": 27, + "name": "Dickson Shepherd", + "gender": "male", + "company": "COMTOURS", + "email": "dicksonshepherd@comtours.com", + "phone": "+1 (871) 458-2972" + }, + { + "_id": "55d2fc862065aeaf17581fa6", + "age": 40, + "name": "Fowler Adams", + "gender": "male", + "company": "NEPTIDE", + "email": "fowleradams@neptide.com", + "phone": "+1 (836) 431-3585" + }, + { + "_id": "55d2fc866aab8b242a30acfd", + "age": 22, + "name": "Durham Frost", + "gender": "male", + "company": "SNOWPOKE", + "email": "durhamfrost@snowpoke.com", + "phone": "+1 (801) 510-2084" + }, + { + "_id": "55d2fc86d3a4aafedc274ffa", + "age": 27, + "name": "Delia Barnes", + "gender": "female", + "company": "CONFERIA", + "email": "deliabarnes@conferia.com", + "phone": "+1 (999) 471-2182" + }, + { + "_id": "55d2fc8601ed1a317e289785", + "age": 27, + "name": "Gill Tillman", + "gender": "male", + "company": "MATRIXITY", + "email": "gilltillman@matrixity.com", + "phone": "+1 (880) 419-3960" + }, + { + "_id": "55d2fc86098dda4fa3812793", + "age": 27, + "name": "Loraine Saunders", + "gender": "female", + "company": "DOGTOWN", + "email": "lorainesaunders@dogtown.com", + "phone": "+1 (868) 500-3061" + }, + { + "_id": "55d2fc865f9615b558837892", + "age": 34, + "name": "Lou Mcdonald", + "gender": "female", + "company": "COMCUBINE", + "email": "loumcdonald@comcubine.com", + "phone": "+1 (857) 568-3427" + }, + { + "_id": "55d2fc865a5a752bb88dbcd8", + "age": 28, + "name": "Beth Lester", + "gender": "female", + "company": "EARTHWAX", + "email": "bethlester@earthwax.com", + "phone": "+1 (805) 565-2363" + }, + { + "_id": "55d2fc862e7d03a2f76c9e58", + "age": 37, + "name": "Karin Mason", + "gender": "female", + "company": "GEEKNET", + "email": "karinmason@geeknet.com", + "phone": "+1 (923) 438-3684" + }, + { + "_id": "55d2fc8617970ba4987ed4b3", + "age": 39, + "name": "Jeanine Watson", + "gender": "female", + "company": "LINGOAGE", + "email": "jeaninewatson@lingoage.com", + "phone": "+1 (919) 444-3722" + }, + { + "_id": "55d2fc86445181571721c6c9", + "age": 20, + "name": "Soto Wilkins", + "gender": "male", + "company": "JUMPSTACK", + "email": "sotowilkins@jumpstack.com", + "phone": "+1 (818) 518-3028" + }, + { + "_id": "55d2fc86b8b146361e17fa83", + "age": 20, + "name": "Mabel Fields", + "gender": "female", + "company": "MAGNINA", + "email": "mabelfields@magnina.com", + "phone": "+1 (861) 583-2161" + }, + { + "_id": "55d2fc8667abfe5721a74176", + "age": 26, + "name": "Ruthie Bailey", + "gender": "female", + "company": "ROCKLOGIC", + "email": "ruthiebailey@rocklogic.com", + "phone": "+1 (872) 402-3619" + }, + { + "_id": "55d2fc865cfa092adb20dc0f", + "age": 22, + "name": "Vaughan Vincent", + "gender": "male", + "company": "ACIUM", + "email": "vaughanvincent@acium.com", + "phone": "+1 (919) 548-3948" + }, + { + "_id": "55d2fc86fe25d347c15e1749", + "age": 38, + "name": "Myrtle Burris", + "gender": "female", + "company": "IRACK", + "email": "myrtleburris@irack.com", + "phone": "+1 (840) 526-2646" + }, + { + "_id": "55d2fc8689cd98b47e44118d", + "age": 37, + "name": "Peggy Mercer", + "gender": "female", + "company": "ESCENTA", + "email": "peggymercer@escenta.com", + "phone": "+1 (817) 574-3310" + }, + { + "_id": "55d2fc86ae662f567ae09404", + "age": 24, + "name": "Mollie Simmons", + "gender": "female", + "company": "STOCKPOST", + "email": "molliesimmons@stockpost.com", + "phone": "+1 (854) 461-2851" + }, + { + "_id": "55d2fc86f738f2bd6ebebd1c", + "age": 37, + "name": "Case Cox", + "gender": "male", + "company": "ACCEL", + "email": "casecox@accel.com", + "phone": "+1 (853) 473-2780" + }, + { + "_id": "55d2fc86c80907be4ff386d5", + "age": 26, + "name": "Snyder Mcclure", + "gender": "male", + "company": "PLASMOX", + "email": "snydermcclure@plasmox.com", + "phone": "+1 (980) 583-3213" + }, + { + "_id": "55d2fc86ed956580fa2cff25", + "age": 31, + "name": "Kelly Malone", + "gender": "male", + "company": "XANIDE", + "email": "kellymalone@xanide.com", + "phone": "+1 (871) 436-2431" + }, + { + "_id": "55d2fc86fcd2b8d749bd1feb", + "age": 20, + "name": "Jackie Carr", + "gender": "female", + "company": "VOIPA", + "email": "jackiecarr@voipa.com", + "phone": "+1 (807) 431-3436" + }, + { + "_id": "55d2fc86d9aac03a007489ef", + "age": 34, + "name": "Cochran Walter", + "gender": "male", + "company": "NIKUDA", + "email": "cochranwalter@nikuda.com", + "phone": "+1 (977) 410-3770" + }, + { + "_id": "55d2fc86140fefcd667c533f", + "age": 22, + "name": "Ellen Ortiz", + "gender": "female", + "company": "NEWCUBE", + "email": "ellenortiz@newcube.com", + "phone": "+1 (980) 541-3099" + }, + { + "_id": "55d2fc86d374c6a649e5877e", + "age": 35, + "name": "Concetta Beard", + "gender": "female", + "company": "PLASMOS", + "email": "concettabeard@plasmos.com", + "phone": "+1 (839) 539-2423" + }, + { + "_id": "55d2fc861ee809861b7c2b38", + "age": 33, + "name": "Josephine Alexander", + "gender": "female", + "company": "LOVEPAD", + "email": "josephinealexander@lovepad.com", + "phone": "+1 (880) 452-2208" + }, + { + "_id": "55d2fc8677db4f8446d43041", + "age": 39, + "name": "Melisa Dean", + "gender": "female", + "company": "GEOFORM", + "email": "melisadean@geoform.com", + "phone": "+1 (934) 452-2532" + }, + { + "_id": "55d2fc861dc0856e2ec744ab", + "age": 26, + "name": "Morgan Galloway", + "gender": "male", + "company": "ELEMANTRA", + "email": "morgangalloway@elemantra.com", + "phone": "+1 (888) 461-2261" + }, + { + "_id": "55d2fc869bc97691cec7d569", + "age": 26, + "name": "Curtis Griffith", + "gender": "male", + "company": "TINGLES", + "email": "curtisgriffith@tingles.com", + "phone": "+1 (881) 517-2174" + }, + { + "_id": "55d2fc8624b80b5986e1de83", + "age": 40, + "name": "Gentry Mccarthy", + "gender": "male", + "company": "GEEKOLA", + "email": "gentrymccarthy@geekola.com", + "phone": "+1 (908) 559-3049" + }, + { + "_id": "55d2fc869c2a158da79f9a7f", + "age": 37, + "name": "Lancaster Justice", + "gender": "male", + "company": "NAXDIS", + "email": "lancasterjustice@naxdis.com", + "phone": "+1 (980) 456-3515" + }, + { + "_id": "55d2fc867c8e30a12fddcb79", + "age": 28, + "name": "Jenifer Barr", + "gender": "female", + "company": "ZORROMOP", + "email": "jeniferbarr@zorromop.com", + "phone": "+1 (901) 440-3979" + }, + { + "_id": "55d2fc8696d7c8b59507f10c", + "age": 35, + "name": "Benjamin Nolan", + "gender": "male", + "company": "DIGITALUS", + "email": "benjaminnolan@digitalus.com", + "phone": "+1 (828) 582-3041" + }, + { + "_id": "55d2fc86b7e719a5c07b9f9e", + "age": 27, + "name": "Beach Valentine", + "gender": "male", + "company": "KAGGLE", + "email": "beachvalentine@kaggle.com", + "phone": "+1 (961) 563-3631" + }, + { + "_id": "55d2fc860bb827b4eab70038", + "age": 27, + "name": "Brady Moore", + "gender": "male", + "company": "CORPULSE", + "email": "bradymoore@corpulse.com", + "phone": "+1 (859) 434-2962" + }, + { + "_id": "55d2fc862fc00a2ce6ab7f32", + "age": 27, + "name": "Page Ray", + "gender": "male", + "company": "BEZAL", + "email": "pageray@bezal.com", + "phone": "+1 (845) 492-2182" + }, + { + "_id": "55d2fc861b57330b56d5b3f8", + "age": 40, + "name": "Lupe Gould", + "gender": "female", + "company": "DOGNOSIS", + "email": "lupegould@dognosis.com", + "phone": "+1 (955) 579-2141" + }, + { + "_id": "55d2fc864a8fabbf89d106cd", + "age": 23, + "name": "Melva Abbott", + "gender": "female", + "company": "CODACT", + "email": "melvaabbott@codact.com", + "phone": "+1 (801) 478-2678" + }, + { + "_id": "55d2fc86107ad0ad66c9d3ea", + "age": 22, + "name": "Mendez Middleton", + "gender": "male", + "company": "CUJO", + "email": "mendezmiddleton@cujo.com", + "phone": "+1 (908) 430-2032" + }, + { + "_id": "55d2fc86d2f5edd1222e87cc", + "age": 31, + "name": "Meredith Ayers", + "gender": "female", + "company": "OPTYK", + "email": "meredithayers@optyk.com", + "phone": "+1 (875) 472-2514" + }, + { + "_id": "55d2fc86551f2a796de0f3ad", + "age": 21, + "name": "Burns Serrano", + "gender": "male", + "company": "ZOUNDS", + "email": "burnsserrano@zounds.com", + "phone": "+1 (939) 558-2221" + }, + { + "_id": "55d2fc86540849ad56aeac98", + "age": 30, + "name": "Barr Sykes", + "gender": "male", + "company": "CORECOM", + "email": "barrsykes@corecom.com", + "phone": "+1 (982) 523-3577" + }, + { + "_id": "55d2fc86e7d43d06e4d135f2", + "age": 28, + "name": "Julie Johnson", + "gender": "female", + "company": "BIOTICA", + "email": "juliejohnson@biotica.com", + "phone": "+1 (918) 487-3230" + }, + { + "_id": "55d2fc86450521acf4a465d9", + "age": 23, + "name": "Dawn Vinson", + "gender": "female", + "company": "MENBRAIN", + "email": "dawnvinson@menbrain.com", + "phone": "+1 (936) 525-3273" + }, + { + "_id": "55d2fc86df3e093a104f498b", + "age": 30, + "name": "Ginger Ryan", + "gender": "female", + "company": "ENTHAZE", + "email": "gingerryan@enthaze.com", + "phone": "+1 (865) 530-2726" + }, + { + "_id": "55d2fc86b6a6fed233908500", + "age": 40, + "name": "Mcconnell Prince", + "gender": "male", + "company": "TRIPSCH", + "email": "mcconnellprince@tripsch.com", + "phone": "+1 (923) 586-2117" + }, + { + "_id": "55d2fc8619de2514561b7ac1", + "age": 23, + "name": "Peck Blackwell", + "gender": "male", + "company": "ASSISTIA", + "email": "peckblackwell@assistia.com", + "phone": "+1 (988) 549-3418" + }, + { + "_id": "55d2fc8619b2c263aec32e51", + "age": 32, + "name": "Simmons Benton", + "gender": "male", + "company": "TROLLERY", + "email": "simmonsbenton@trollery.com", + "phone": "+1 (924) 439-2962" + }, + { + "_id": "55d2fc8612c77e18fa3c743d", + "age": 36, + "name": "Mari Silva", + "gender": "female", + "company": "MAGMINA", + "email": "marisilva@magmina.com", + "phone": "+1 (863) 509-3186" + }, + { + "_id": "55d2fc86226a40ce862e518e", + "age": 40, + "name": "Erin Jefferson", + "gender": "female", + "company": "VIAGREAT", + "email": "erinjefferson@viagreat.com", + "phone": "+1 (921) 408-2295" + }, + { + "_id": "55d2fc8601013536fc55a097", + "age": 37, + "name": "Nellie Ballard", + "gender": "female", + "company": "GEOFORMA", + "email": "nellieballard@geoforma.com", + "phone": "+1 (918) 406-2600" + }, + { + "_id": "55d2fc864402965704b3453a", + "age": 27, + "name": "Austin Brewer", + "gender": "male", + "company": "METROZ", + "email": "austinbrewer@metroz.com", + "phone": "+1 (968) 584-2959" + }, + { + "_id": "55d2fc8690a7d6957a09f6c8", + "age": 34, + "name": "Pickett Buckley", + "gender": "male", + "company": "SUREPLEX", + "email": "pickettbuckley@sureplex.com", + "phone": "+1 (975) 520-3259" + }, + { + "_id": "55d2fc8695f3020c96b14f95", + "age": 39, + "name": "Coleen Herman", + "gender": "female", + "company": "NORALI", + "email": "coleenherman@norali.com", + "phone": "+1 (916) 506-2704" + }, + { + "_id": "55d2fc869b58b96d1d2cdfcc", + "age": 37, + "name": "Roy Guerrero", + "gender": "male", + "company": "PLUTORQUE", + "email": "royguerrero@plutorque.com", + "phone": "+1 (922) 541-3741" + }, + { + "_id": "55d2fc86ba1ed1189e9020ee", + "age": 29, + "name": "Oneal Curtis", + "gender": "male", + "company": "DATAGEN", + "email": "onealcurtis@datagen.com", + "phone": "+1 (847) 421-3483" + }, + { + "_id": "55d2fc86b5c5e89eb9fe1e79", + "age": 29, + "name": "Chaney Christian", + "gender": "male", + "company": "XPLOR", + "email": "chaneychristian@xplor.com", + "phone": "+1 (847) 517-3918" + }, + { + "_id": "55d2fc86f6b5a3d91952d941", + "age": 29, + "name": "Cantu Richard", + "gender": "male", + "company": "ZANYMAX", + "email": "canturichard@zanymax.com", + "phone": "+1 (972) 487-2616" + }, + { + "_id": "55d2fc8609b0f98cbc2e101d", + "age": 37, + "name": "Newton Barrera", + "gender": "male", + "company": "PORTALINE", + "email": "newtonbarrera@portaline.com", + "phone": "+1 (964) 527-3130" + }, + { + "_id": "55d2fc86d0e063ca7629c11b", + "age": 28, + "name": "Jodi Pollard", + "gender": "female", + "company": "GOKO", + "email": "jodipollard@goko.com", + "phone": "+1 (957) 468-2658" + }, + { + "_id": "55d2fc8688d95c5c579bd328", + "age": 35, + "name": "Effie Nunez", + "gender": "female", + "company": "SNACKTION", + "email": "effienunez@snacktion.com", + "phone": "+1 (805) 576-3749" + }, + { + "_id": "55d2fc86f36c0eab69222b17", + "age": 38, + "name": "Haley Battle", + "gender": "male", + "company": "TERRAGEN", + "email": "haleybattle@terragen.com", + "phone": "+1 (955) 581-3931" + }, + { + "_id": "55d2fc86ee6900b197860a35", + "age": 29, + "name": "Cathy Vaughn", + "gender": "female", + "company": "CANOPOLY", + "email": "cathyvaughn@canopoly.com", + "phone": "+1 (875) 539-3578" + }, + { + "_id": "55d2fc86613f698b355e87bc", + "age": 26, + "name": "Mendoza Maxwell", + "gender": "male", + "company": "TERAPRENE", + "email": "mendozamaxwell@teraprene.com", + "phone": "+1 (820) 471-3500" + }, + { + "_id": "55d2fc86f86a80f78c45936a", + "age": 28, + "name": "Rosetta Hughes", + "gender": "female", + "company": "DEVILTOE", + "email": "rosettahughes@deviltoe.com", + "phone": "+1 (911) 418-2439" + }, + { + "_id": "55d2fc8683d3c4a377a984a5", + "age": 28, + "name": "Frazier Larson", + "gender": "male", + "company": "DAIDO", + "email": "frazierlarson@daido.com", + "phone": "+1 (995) 459-3756" + }, + { + "_id": "55d2fc8669baef03c4d6a4a5", + "age": 27, + "name": "Brittney Ratliff", + "gender": "female", + "company": "TALKALOT", + "email": "brittneyratliff@talkalot.com", + "phone": "+1 (865) 568-2986" + }, + { + "_id": "55d2fc86404583ccc6b6a4f4", + "age": 21, + "name": "Hancock Gilliam", + "gender": "male", + "company": "EXPOSA", + "email": "hancockgilliam@exposa.com", + "phone": "+1 (894) 519-3139" + }, + { + "_id": "55d2fc8675478e0b84bb5ab3", + "age": 20, + "name": "Lilia Mccormick", + "gender": "female", + "company": "DENTREX", + "email": "liliamccormick@dentrex.com", + "phone": "+1 (997) 552-3944" + }, + { + "_id": "55d2fc8658bf3954cdf52ebb", + "age": 20, + "name": "Chrystal Mcneil", + "gender": "female", + "company": "LIMOZEN", + "email": "chrystalmcneil@limozen.com", + "phone": "+1 (943) 519-2952" + }, + { + "_id": "55d2fc865ebec9e8c7adc383", + "age": 31, + "name": "Hebert Alston", + "gender": "male", + "company": "IZZBY", + "email": "hebertalston@izzby.com", + "phone": "+1 (953) 482-2029" + }, + { + "_id": "55d2fc86bf1085e6fd2e9ea1", + "age": 23, + "name": "Mcfarland Carrillo", + "gender": "male", + "company": "EQUITOX", + "email": "mcfarlandcarrillo@equitox.com", + "phone": "+1 (999) 478-3822" + }, + { + "_id": "55d2fc8641d4f2d09f539bd6", + "age": 40, + "name": "Porter Weaver", + "gender": "male", + "company": "QUILM", + "email": "porterweaver@quilm.com", + "phone": "+1 (831) 501-2739" + }, + { + "_id": "55d2fc869ee5331a50039e30", + "age": 23, + "name": "Dale Sims", + "gender": "male", + "company": "SCENTRIC", + "email": "dalesims@scentric.com", + "phone": "+1 (845) 597-2120" + }, + { + "_id": "55d2fc86cb96d31fe71e4a18", + "age": 22, + "name": "William Dixon", + "gender": "male", + "company": "NAMEBOX", + "email": "williamdixon@namebox.com", + "phone": "+1 (970) 448-2651" + }, + { + "_id": "55d2fc86e88daeb1e198671d", + "age": 33, + "name": "Patrica Reed", + "gender": "female", + "company": "CORPORANA", + "email": "patricareed@corporana.com", + "phone": "+1 (817) 457-2413" + }, + { + "_id": "55d2fc86c33d9f422bf8bfcc", + "age": 20, + "name": "Marylou Mcmillan", + "gender": "female", + "company": "RETRACK", + "email": "maryloumcmillan@retrack.com", + "phone": "+1 (908) 568-2328" + }, + { + "_id": "55d2fc86c4b1b139887e76f1", + "age": 20, + "name": "Maritza David", + "gender": "female", + "company": "ORBEAN", + "email": "maritzadavid@orbean.com", + "phone": "+1 (851) 503-3737" + }, + { + "_id": "55d2fc86ac0f360d91c740e6", + "age": 23, + "name": "Lorie Moses", + "gender": "female", + "company": "ROCKABYE", + "email": "loriemoses@rockabye.com", + "phone": "+1 (823) 431-2387" + }, + { + "_id": "55d2fc86d7d406569b386b17", + "age": 35, + "name": "Shields Weiss", + "gender": "male", + "company": "RODEOCEAN", + "email": "shieldsweiss@rodeocean.com", + "phone": "+1 (957) 490-3725" + }, + { + "_id": "55d2fc86e4f66ce770ae2c93", + "age": 25, + "name": "Eugenia Berry", + "gender": "female", + "company": "BISBA", + "email": "eugeniaberry@bisba.com", + "phone": "+1 (948) 403-3403" + }, + { + "_id": "55d2fc8618f2f4f428223522", + "age": 33, + "name": "Howard Compton", + "gender": "male", + "company": "NAMEGEN", + "email": "howardcompton@namegen.com", + "phone": "+1 (827) 439-3667" + }, + { + "_id": "55d2fc86f5adc0f6dd535ea6", + "age": 28, + "name": "Key Davis", + "gender": "male", + "company": "PORTICO", + "email": "keydavis@portico.com", + "phone": "+1 (873) 533-2980" + }, + { + "_id": "55d2fc86876669f4e9431417", + "age": 33, + "name": "Phillips Solis", + "gender": "male", + "company": "DANCITY", + "email": "phillipssolis@dancity.com", + "phone": "+1 (883) 481-3114" + }, + { + "_id": "55d2fc86f2bb610d7ad9ea36", + "age": 40, + "name": "Cash Pugh", + "gender": "male", + "company": "STUCCO", + "email": "cashpugh@stucco.com", + "phone": "+1 (873) 512-2106" + }, + { + "_id": "55d2fc863be1649d5bd3be39", + "age": 21, + "name": "Elinor Warner", + "gender": "female", + "company": "FOSSIEL", + "email": "elinorwarner@fossiel.com", + "phone": "+1 (950) 431-3679" + }, + { + "_id": "55d2fc86fdad2af5536237e2", + "age": 35, + "name": "Jacquelyn Doyle", + "gender": "female", + "company": "CYTREX", + "email": "jacquelyndoyle@cytrex.com", + "phone": "+1 (924) 569-2919" + }, + { + "_id": "55d2fc86f3affa20ab27edff", + "age": 33, + "name": "Jeannine Mosley", + "gender": "female", + "company": "ACUSAGE", + "email": "jeanninemosley@acusage.com", + "phone": "+1 (954) 517-2805" + }, + { + "_id": "55d2fc8670dd0dbdd6e4d195", + "age": 37, + "name": "Logan Brady", + "gender": "male", + "company": "TELLIFLY", + "email": "loganbrady@tellifly.com", + "phone": "+1 (861) 576-2313" + }, + { + "_id": "55d2fc86b9a15e4721982a39", + "age": 26, + "name": "Houston Joseph", + "gender": "male", + "company": "BOILICON", + "email": "houstonjoseph@boilicon.com", + "phone": "+1 (822) 519-3430" + }, + { + "_id": "55d2fc86f225999b0b8742d2", + "age": 38, + "name": "Rita Lindsey", + "gender": "female", + "company": "FIBEROX", + "email": "ritalindsey@fiberox.com", + "phone": "+1 (805) 551-3755" + }, + { + "_id": "55d2fc86e9dad38b6873b807", + "age": 22, + "name": "Strong Poole", + "gender": "male", + "company": "KINDALOO", + "email": "strongpoole@kindaloo.com", + "phone": "+1 (918) 426-2076" + }, + { + "_id": "55d2fc861608b965b2283827", + "age": 38, + "name": "Hines Mathews", + "gender": "male", + "company": "INTRADISK", + "email": "hinesmathews@intradisk.com", + "phone": "+1 (932) 420-2236" + }, + { + "_id": "55d2fc863079075f91241a16", + "age": 28, + "name": "Trina Wiley", + "gender": "female", + "company": "HATOLOGY", + "email": "trinawiley@hatology.com", + "phone": "+1 (855) 466-3287" + }, + { + "_id": "55d2fc86f2fae1a79253fb61", + "age": 23, + "name": "Kirby Tucker", + "gender": "male", + "company": "AQUAMATE", + "email": "kirbytucker@aquamate.com", + "phone": "+1 (935) 456-3272" + }, + { + "_id": "55d2fc86c42bf49f8202b2fa", + "age": 28, + "name": "Ballard Stein", + "gender": "male", + "company": "KOOGLE", + "email": "ballardstein@koogle.com", + "phone": "+1 (943) 586-2225" + }, + { + "_id": "55d2fc865db815da198c0776", + "age": 36, + "name": "Wagner Mcfarland", + "gender": "male", + "company": "ACCIDENCY", + "email": "wagnermcfarland@accidency.com", + "phone": "+1 (920) 533-2157" + }, + { + "_id": "55d2fc866aeb268fe48fd6be", + "age": 22, + "name": "Wiley Wilder", + "gender": "male", + "company": "KIDSTOCK", + "email": "wileywilder@kidstock.com", + "phone": "+1 (957) 459-3416" + }, + { + "_id": "55d2fc8606f67a423d303437", + "age": 37, + "name": "Rosario Slater", + "gender": "female", + "company": "SPRINGBEE", + "email": "rosarioslater@springbee.com", + "phone": "+1 (950) 506-3454" + }, + { + "_id": "55d2fc86510fd16a269a0201", + "age": 37, + "name": "Walker Mcdowell", + "gender": "male", + "company": "ONTAGENE", + "email": "walkermcdowell@ontagene.com", + "phone": "+1 (953) 579-3429" + }, + { + "_id": "55d2fc867d419d30f9394f56", + "age": 32, + "name": "Booth Pratt", + "gender": "male", + "company": "ZIGGLES", + "email": "boothpratt@ziggles.com", + "phone": "+1 (835) 453-3707" + }, + { + "_id": "55d2fc86e631b8f71bbe7b35", + "age": 33, + "name": "Georgia Carpenter", + "gender": "female", + "company": "STRALUM", + "email": "georgiacarpenter@stralum.com", + "phone": "+1 (923) 536-3557" + }, + { + "_id": "55d2fc866df6437afa4cfaf6", + "age": 26, + "name": "Harding Powers", + "gender": "male", + "company": "BEADZZA", + "email": "hardingpowers@beadzza.com", + "phone": "+1 (855) 467-2993" + }, + { + "_id": "55d2fc867ade492afcfc24a6", + "age": 37, + "name": "Kaye Brown", + "gender": "female", + "company": "AMTAS", + "email": "kayebrown@amtas.com", + "phone": "+1 (926) 444-3936" + }, + { + "_id": "55d2fc862bf33dd3169710ff", + "age": 24, + "name": "Mccray Padilla", + "gender": "male", + "company": "FUTURIS", + "email": "mccraypadilla@futuris.com", + "phone": "+1 (969) 561-3819" + }, + { + "_id": "55d2fc8666f74690303abf65", + "age": 35, + "name": "Moon Moss", + "gender": "male", + "company": "EURON", + "email": "moonmoss@euron.com", + "phone": "+1 (885) 514-2872" + }, + { + "_id": "55d2fc860a8b5abfdf57bd37", + "age": 26, + "name": "Lane Gregory", + "gender": "male", + "company": "SKINSERVE", + "email": "lanegregory@skinserve.com", + "phone": "+1 (818) 455-3048" + }, + { + "_id": "55d2fc86f17541fe3b770b26", + "age": 30, + "name": "Cummings Good", + "gender": "male", + "company": "GEEKOLOGY", + "email": "cummingsgood@geekology.com", + "phone": "+1 (821) 426-3476" + }, + { + "_id": "55d2fc865b6232d788278e1f", + "age": 26, + "name": "Lottie Soto", + "gender": "female", + "company": "INTERGEEK", + "email": "lottiesoto@intergeek.com", + "phone": "+1 (905) 516-2928" + }, + { + "_id": "55d2fc868388a50b97dda5c2", + "age": 38, + "name": "Bridges Bell", + "gender": "male", + "company": "MIRACULA", + "email": "bridgesbell@miracula.com", + "phone": "+1 (917) 438-3079" + }, + { + "_id": "55d2fc86cc2120d10b75c41b", + "age": 23, + "name": "Marcella Lancaster", + "gender": "female", + "company": "NAVIR", + "email": "marcellalancaster@navir.com", + "phone": "+1 (851) 478-2535" + }, + { + "_id": "55d2fc86f52bd008c87c6993", + "age": 32, + "name": "Foley Yang", + "gender": "male", + "company": "APEXTRI", + "email": "foleyyang@apextri.com", + "phone": "+1 (978) 504-2003" + }, + { + "_id": "55d2fc86088b65117b293eef", + "age": 21, + "name": "Debora Levine", + "gender": "female", + "company": "VANTAGE", + "email": "deboralevine@vantage.com", + "phone": "+1 (820) 472-2507" + }, + { + "_id": "55d2fc86765d079d8584c281", + "age": 30, + "name": "Jill Durham", + "gender": "female", + "company": "FUTURITY", + "email": "jilldurham@futurity.com", + "phone": "+1 (996) 499-2910" + }, + { + "_id": "55d2fc860ed183243d043f79", + "age": 28, + "name": "Della Sherman", + "gender": "female", + "company": "EXTRO", + "email": "dellasherman@extro.com", + "phone": "+1 (893) 541-2867" + }, + { + "_id": "55d2fc8646733a05fa448c6e", + "age": 30, + "name": "Tamara Albert", + "gender": "female", + "company": "ECOLIGHT", + "email": "tamaraalbert@ecolight.com", + "phone": "+1 (870) 514-2615" + }, + { + "_id": "55d2fc86b8bf0a0f7ffb702e", + "age": 39, + "name": "Lynn Green", + "gender": "male", + "company": "SNIPS", + "email": "lynngreen@snips.com", + "phone": "+1 (938) 464-2073" + }, + { + "_id": "55d2fc863e577905fc3ea8e7", + "age": 29, + "name": "Barbra Tate", + "gender": "female", + "company": "ACRUEX", + "email": "barbratate@acruex.com", + "phone": "+1 (809) 418-2604" + }, + { + "_id": "55d2fc86335b53151fc242b5", + "age": 33, + "name": "Potts Dickerson", + "gender": "male", + "company": "SHADEASE", + "email": "pottsdickerson@shadease.com", + "phone": "+1 (967) 539-3330" + }, + { + "_id": "55d2fc86716df5cb28925d59", + "age": 36, + "name": "Nancy Woodard", + "gender": "female", + "company": "ZOSIS", + "email": "nancywoodard@zosis.com", + "phone": "+1 (811) 434-3223" + }, + { + "_id": "55d2fc86058113d9a4909796", + "age": 29, + "name": "Park Evans", + "gender": "male", + "company": "XUMONK", + "email": "parkevans@xumonk.com", + "phone": "+1 (836) 443-2361" + }, + { + "_id": "55d2fc8659b6d92fb2880f83", + "age": 25, + "name": "Nicole Sullivan", + "gender": "female", + "company": "QUALITEX", + "email": "nicolesullivan@qualitex.com", + "phone": "+1 (823) 584-2994" + }, + { + "_id": "55d2fc86510772cfe78617a9", + "age": 33, + "name": "Bowers Barnett", + "gender": "male", + "company": "HOUSEDOWN", + "email": "bowersbarnett@housedown.com", + "phone": "+1 (872) 466-3548" + }, + { + "_id": "55d2fc868dec3ac619cfc262", + "age": 21, + "name": "Jeri Nielsen", + "gender": "female", + "company": "MOBILDATA", + "email": "jerinielsen@mobildata.com", + "phone": "+1 (886) 581-2045" + }, + { + "_id": "55d2fc863e89d05123e90aaf", + "age": 26, + "name": "Delores Farmer", + "gender": "female", + "company": "XERONK", + "email": "deloresfarmer@xeronk.com", + "phone": "+1 (872) 556-2716" + }, + { + "_id": "55d2fc8618c10cef39c48f97", + "age": 38, + "name": "Mathis Walsh", + "gender": "male", + "company": "VURBO", + "email": "mathiswalsh@vurbo.com", + "phone": "+1 (837) 459-2909" + }, + { + "_id": "55d2fc868b3487cb71c8bac4", + "age": 20, + "name": "Ingrid Shelton", + "gender": "female", + "company": "ORBIN", + "email": "ingridshelton@orbin.com", + "phone": "+1 (914) 592-2364" + }, + { + "_id": "55d2fc86bcf5c885edacef50", + "age": 36, + "name": "Socorro Burns", + "gender": "female", + "company": "NETAGY", + "email": "socorroburns@netagy.com", + "phone": "+1 (931) 523-3116" + }, + { + "_id": "55d2fc8693f704f5c95c48a7", + "age": 39, + "name": "Jo Ware", + "gender": "female", + "company": "FLYBOYZ", + "email": "joware@flyboyz.com", + "phone": "+1 (844) 467-2192" + }, + { + "_id": "55d2fc86e87485075df33029", + "age": 38, + "name": "Emilia Flores", + "gender": "female", + "company": "ZAGGLES", + "email": "emiliaflores@zaggles.com", + "phone": "+1 (992) 408-2629" + }, + { + "_id": "55d2fc861ab925ca0b9b15c6", + "age": 31, + "name": "Burks Haney", + "gender": "male", + "company": "ZYPLE", + "email": "burkshaney@zyple.com", + "phone": "+1 (882) 401-2811" + }, + { + "_id": "55d2fc86e9c252588455b811", + "age": 31, + "name": "Holly Snow", + "gender": "female", + "company": "FURNAFIX", + "email": "hollysnow@furnafix.com", + "phone": "+1 (802) 592-2798" + }, + { + "_id": "55d2fc86e07c753096021423", + "age": 25, + "name": "Frances Hayden", + "gender": "female", + "company": "SNORUS", + "email": "franceshayden@snorus.com", + "phone": "+1 (818) 481-2431" + }, + { + "_id": "55d2fc86496884876065c346", + "age": 28, + "name": "Lila Lewis", + "gender": "female", + "company": "ZOMBOID", + "email": "lilalewis@zomboid.com", + "phone": "+1 (893) 593-2423" + }, + { + "_id": "55d2fc865c6ea080a4d0a5b5", + "age": 34, + "name": "Torres Finley", + "gender": "male", + "company": "MAXIMIND", + "email": "torresfinley@maximind.com", + "phone": "+1 (888) 504-3674" + }, + { + "_id": "55d2fc86391c44c545fe989f", + "age": 23, + "name": "Horne James", + "gender": "male", + "company": "PETICULAR", + "email": "hornejames@peticular.com", + "phone": "+1 (865) 558-2517" + }, + { + "_id": "55d2fc86c8c2529939eada03", + "age": 28, + "name": "Jennings Wallace", + "gender": "male", + "company": "CAXT", + "email": "jenningswallace@caxt.com", + "phone": "+1 (833) 599-3895" + }, + { + "_id": "55d2fc86ee656fe865fbde29", + "age": 33, + "name": "Lily Gilmore", + "gender": "female", + "company": "RADIANTIX", + "email": "lilygilmore@radiantix.com", + "phone": "+1 (854) 561-3148" + }, + { + "_id": "55d2fc868455126d596ab537", + "age": 35, + "name": "Kristy Delacruz", + "gender": "female", + "company": "EQUICOM", + "email": "kristydelacruz@equicom.com", + "phone": "+1 (871) 502-3732" + }, + { + "_id": "55d2fc863096983ef370ca49", + "age": 31, + "name": "England Vance", + "gender": "male", + "company": "VOLAX", + "email": "englandvance@volax.com", + "phone": "+1 (840) 593-3417" + }, + { + "_id": "55d2fc8671f3c1e30bee852f", + "age": 23, + "name": "Rodriguez Foreman", + "gender": "male", + "company": "EXTRAWEAR", + "email": "rodriguezforeman@extrawear.com", + "phone": "+1 (804) 497-2101" + }, + { + "_id": "55d2fc86bea38e2b0cd970cb", + "age": 35, + "name": "Richard Garrett", + "gender": "male", + "company": "SUPREMIA", + "email": "richardgarrett@supremia.com", + "phone": "+1 (925) 461-3414" + }, + { + "_id": "55d2fc862c5193ab7b4668b7", + "age": 40, + "name": "Connie Ortega", + "gender": "female", + "company": "ZILODYNE", + "email": "connieortega@zilodyne.com", + "phone": "+1 (838) 582-3241" + }, + { + "_id": "55d2fc865a6b25d6bc09180e", + "age": 24, + "name": "Solomon Bates", + "gender": "male", + "company": "EXOSPACE", + "email": "solomonbates@exospace.com", + "phone": "+1 (897) 496-2243" + }, + { + "_id": "55d2fc86a16a4e077136e4b5", + "age": 32, + "name": "Valencia Andrews", + "gender": "male", + "company": "NORSUP", + "email": "valenciaandrews@norsup.com", + "phone": "+1 (891) 503-3593" + }, + { + "_id": "55d2fc86bc5f1bf697a90465", + "age": 24, + "name": "Briggs Vasquez", + "gender": "male", + "company": "ZENTIA", + "email": "briggsvasquez@zentia.com", + "phone": "+1 (802) 415-3377" + }, + { + "_id": "55d2fc86c088d6466d83f8fc", + "age": 26, + "name": "Hester Rice", + "gender": "female", + "company": "CYTREK", + "email": "hesterrice@cytrek.com", + "phone": "+1 (855) 544-3905" + }, + { + "_id": "55d2fc8622fc3e78977288af", + "age": 25, + "name": "Shelly Hendrix", + "gender": "female", + "company": "POWERNET", + "email": "shellyhendrix@powernet.com", + "phone": "+1 (912) 431-2318" + }, + { + "_id": "55d2fc862cdec6cd321df6e0", + "age": 22, + "name": "Alison Newman", + "gender": "female", + "company": "COMTOUR", + "email": "alisonnewman@comtour.com", + "phone": "+1 (836) 582-3513" + }, + { + "_id": "55d2fc86e0070d54d4712ca4", + "age": 30, + "name": "French Rivera", + "gender": "male", + "company": "ACUMENTOR", + "email": "frenchrivera@acumentor.com", + "phone": "+1 (902) 579-2193" + }, + { + "_id": "55d2fc860f64b4423a9d6ffc", + "age": 38, + "name": "Terrell Mendez", + "gender": "male", + "company": "IMAGINART", + "email": "terrellmendez@imaginart.com", + "phone": "+1 (967) 494-2713" + }, + { + "_id": "55d2fc86548793116b225b3b", + "age": 30, + "name": "Parsons Robertson", + "gender": "male", + "company": "OBONES", + "email": "parsonsrobertson@obones.com", + "phone": "+1 (984) 434-2810" + }, + { + "_id": "55d2fc865b8b0d6a59db876c", + "age": 33, + "name": "Livingston Barry", + "gender": "male", + "company": "TYPHONICA", + "email": "livingstonbarry@typhonica.com", + "phone": "+1 (976) 560-3878" + }, + { + "_id": "55d2fc865ce23fb8b34cae53", + "age": 40, + "name": "Valeria Stout", + "gender": "female", + "company": "AVENETRO", + "email": "valeriastout@avenetro.com", + "phone": "+1 (885) 557-3624" + }, + { + "_id": "55d2fc86c03ea1d6e81563ac", + "age": 39, + "name": "Grimes Dyer", + "gender": "male", + "company": "GEOLOGIX", + "email": "grimesdyer@geologix.com", + "phone": "+1 (896) 533-2919" + }, + { + "_id": "55d2fc8655c0acb356a06c8f", + "age": 29, + "name": "Higgins Short", + "gender": "male", + "company": "BICOL", + "email": "higginsshort@bicol.com", + "phone": "+1 (976) 444-3073" + }, + { + "_id": "55d2fc865b6db005487c52bb", + "age": 34, + "name": "Gilmore Campos", + "gender": "male", + "company": "PASTURIA", + "email": "gilmorecampos@pasturia.com", + "phone": "+1 (862) 442-2147" + }, + { + "_id": "55d2fc863df4791bcb269217", + "age": 29, + "name": "Sloan Kane", + "gender": "male", + "company": "XELEGYL", + "email": "sloankane@xelegyl.com", + "phone": "+1 (946) 526-2275" + }, + { + "_id": "55d2fc86b2eb7dedbd5a9e8d", + "age": 26, + "name": "Mcpherson Thornton", + "gender": "male", + "company": "KAGE", + "email": "mcphersonthornton@kage.com", + "phone": "+1 (803) 478-2690" + }, + { + "_id": "55d2fc8603f6a8c17148c8dd", + "age": 31, + "name": "Christi Welch", + "gender": "female", + "company": "WARETEL", + "email": "christiwelch@waretel.com", + "phone": "+1 (999) 552-3114" + }, + { + "_id": "55d2fc86118d83cb9d06aa2e", + "age": 29, + "name": "Padilla Travis", + "gender": "male", + "company": "ENERVATE", + "email": "padillatravis@enervate.com", + "phone": "+1 (897) 577-3387" + }, + { + "_id": "55d2fc86aba06801708bed65", + "age": 22, + "name": "Stanton Casey", + "gender": "male", + "company": "BUZZMAKER", + "email": "stantoncasey@buzzmaker.com", + "phone": "+1 (858) 571-2667" + }, + { + "_id": "55d2fc86184810b00043a4b7", + "age": 29, + "name": "Krista Hernandez", + "gender": "female", + "company": "BIOHAB", + "email": "kristahernandez@biohab.com", + "phone": "+1 (832) 510-3654" + }, + { + "_id": "55d2fc86e9c951b5bcea3938", + "age": 36, + "name": "Deleon Oliver", + "gender": "male", + "company": "NETBOOK", + "email": "deleonoliver@netbook.com", + "phone": "+1 (934) 504-2964" + }, + { + "_id": "55d2fc86923000f3ea91ae38", + "age": 36, + "name": "Vasquez Fowler", + "gender": "male", + "company": "ORGANICA", + "email": "vasquezfowler@organica.com", + "phone": "+1 (949) 546-2722" + }, + { + "_id": "55d2fc861e12cd0fa6207a9e", + "age": 33, + "name": "Rutledge Keith", + "gender": "male", + "company": "COLAIRE", + "email": "rutledgekeith@colaire.com", + "phone": "+1 (936) 472-3739" + }, + { + "_id": "55d2fc86927eca39ef0c7ae9", + "age": 26, + "name": "Kirsten Valenzuela", + "gender": "female", + "company": "SEQUITUR", + "email": "kirstenvalenzuela@sequitur.com", + "phone": "+1 (958) 564-3259" + }, + { + "_id": "55d2fc869e922239ce293d2c", + "age": 40, + "name": "Garza Gutierrez", + "gender": "male", + "company": "SPLINX", + "email": "garzagutierrez@splinx.com", + "phone": "+1 (850) 525-3114" + }, + { + "_id": "55d2fc869dd88389d4785283", + "age": 27, + "name": "Shawna Peck", + "gender": "female", + "company": "UNQ", + "email": "shawnapeck@unq.com", + "phone": "+1 (961) 579-3704" + }, + { + "_id": "55d2fc86722d8e2a714bf7f2", + "age": 23, + "name": "Aurelia Mcpherson", + "gender": "female", + "company": "BOINK", + "email": "aureliamcpherson@boink.com", + "phone": "+1 (946) 479-2080" + }, + { + "_id": "55d2fc862324d173c26dfc68", + "age": 39, + "name": "Maryellen Daugherty", + "gender": "female", + "company": "ZILCH", + "email": "maryellendaugherty@zilch.com", + "phone": "+1 (817) 577-3290" + }, + { + "_id": "55d2fc86d311107f869da748", + "age": 35, + "name": "Zelma Hancock", + "gender": "female", + "company": "EVENTAGE", + "email": "zelmahancock@eventage.com", + "phone": "+1 (845) 578-3887" + }, + { + "_id": "55d2fc86afb4ede1f20a6d15", + "age": 34, + "name": "Tessa Adkins", + "gender": "female", + "company": "CONJURICA", + "email": "tessaadkins@conjurica.com", + "phone": "+1 (807) 497-2845" + }, + { + "_id": "55d2fc8648a2e36d6f97fad1", + "age": 22, + "name": "Wong Shaffer", + "gender": "male", + "company": "ACCUFARM", + "email": "wongshaffer@accufarm.com", + "phone": "+1 (865) 589-3833" + }, + { + "_id": "55d2fc86c4c10d58f1fe357f", + "age": 32, + "name": "Ivy Suarez", + "gender": "female", + "company": "UNCORP", + "email": "ivysuarez@uncorp.com", + "phone": "+1 (851) 582-2829" + }, + { + "_id": "55d2fc867c11d1885ca4d8f9", + "age": 25, + "name": "Sosa Barber", + "gender": "male", + "company": "FUELTON", + "email": "sosabarber@fuelton.com", + "phone": "+1 (831) 404-2343" + }, + { + "_id": "55d2fc866405b373cff4d477", + "age": 23, + "name": "Rosalind Craft", + "gender": "female", + "company": "OTHERSIDE", + "email": "rosalindcraft@otherside.com", + "phone": "+1 (847) 455-2079" + }, + { + "_id": "55d2fc863a038b06c712b4b9", + "age": 30, + "name": "Bean Mathis", + "gender": "male", + "company": "QUILK", + "email": "beanmathis@quilk.com", + "phone": "+1 (974) 596-2868" + }, + { + "_id": "55d2fc86922e4a50dfef56ac", + "age": 21, + "name": "Kelley Ruiz", + "gender": "female", + "company": "APEX", + "email": "kelleyruiz@apex.com", + "phone": "+1 (889) 522-2938" + }, + { + "_id": "55d2fc868c7bf8c05a228366", + "age": 22, + "name": "Georgette Chaney", + "gender": "female", + "company": "EXOTECHNO", + "email": "georgettechaney@exotechno.com", + "phone": "+1 (920) 591-3934" + }, + { + "_id": "55d2fc86988c5be0c8ba5412", + "age": 38, + "name": "Tami Bullock", + "gender": "female", + "company": "ISOTRONIC", + "email": "tamibullock@isotronic.com", + "phone": "+1 (828) 567-2857" + }, + { + "_id": "55d2fc862264a1d2de2e8a6b", + "age": 39, + "name": "Castillo Rosario", + "gender": "male", + "company": "KONNECT", + "email": "castillorosario@konnect.com", + "phone": "+1 (938) 402-3484" + }, + { + "_id": "55d2fc8622ee680ff3c522e1", + "age": 31, + "name": "George Weber", + "gender": "male", + "company": "FARMAGE", + "email": "georgeweber@farmage.com", + "phone": "+1 (895) 502-2654" + }, + { + "_id": "55d2fc86a0d45d2916aacb5a", + "age": 28, + "name": "Wheeler Villarreal", + "gender": "male", + "company": "IMPERIUM", + "email": "wheelervillarreal@imperium.com", + "phone": "+1 (889) 507-3796" + }, + { + "_id": "55d2fc866c6e8e85c17c61ca", + "age": 36, + "name": "Arlene Bean", + "gender": "female", + "company": "UNIA", + "email": "arlenebean@unia.com", + "phone": "+1 (970) 463-2147" + }, + { + "_id": "55d2fc86c3c95fd98562a429", + "age": 30, + "name": "Oneil Madden", + "gender": "male", + "company": "COMBOGENE", + "email": "oneilmadden@combogene.com", + "phone": "+1 (849) 507-3555" + }, + { + "_id": "55d2fc86b0f7cc31af45078a", + "age": 35, + "name": "Vaughn Merritt", + "gender": "male", + "company": "ACCUPHARM", + "email": "vaughnmerritt@accupharm.com", + "phone": "+1 (886) 428-2966" + }, + { + "_id": "55d2fc86ad825cb66f2a2feb", + "age": 21, + "name": "Duran Bradford", + "gender": "male", + "company": "SQUISH", + "email": "duranbradford@squish.com", + "phone": "+1 (930) 434-2976" + }, + { + "_id": "55d2fc86efa80c332066194d", + "age": 30, + "name": "Tanisha Knox", + "gender": "female", + "company": "FARMEX", + "email": "tanishaknox@farmex.com", + "phone": "+1 (924) 540-2066" + }, + { + "_id": "55d2fc861dbd55c1fd4bdf23", + "age": 38, + "name": "Esther Foster", + "gender": "female", + "company": "SENSATE", + "email": "estherfoster@sensate.com", + "phone": "+1 (812) 417-2687" + }, + { + "_id": "55d2fc86115b9a01067db6ad", + "age": 35, + "name": "Marion Gray", + "gender": "female", + "company": "HINWAY", + "email": "mariongray@hinway.com", + "phone": "+1 (850) 526-2167" + }, + { + "_id": "55d2fc864b40086c2e4963e2", + "age": 38, + "name": "Ava Flowers", + "gender": "female", + "company": "REVERSUS", + "email": "avaflowers@reversus.com", + "phone": "+1 (989) 415-2504" + }, + { + "_id": "55d2fc8686c59f7395222b11", + "age": 35, + "name": "Katina Burnett", + "gender": "female", + "company": "DUFLEX", + "email": "katinaburnett@duflex.com", + "phone": "+1 (843) 464-3718" + }, + { + "_id": "55d2fc86e012c977bc5b57d6", + "age": 37, + "name": "Ester Cooley", + "gender": "female", + "company": "RUBADUB", + "email": "estercooley@rubadub.com", + "phone": "+1 (856) 407-3009" + }, + { + "_id": "55d2fc865688875c75a158b5", + "age": 36, + "name": "Dennis Mccray", + "gender": "male", + "company": "PETIGEMS", + "email": "dennismccray@petigems.com", + "phone": "+1 (989) 525-3768" + }, + { + "_id": "55d2fc86ca333a1c715a35c7", + "age": 25, + "name": "Mitzi Carson", + "gender": "female", + "company": "KENEGY", + "email": "mitzicarson@kenegy.com", + "phone": "+1 (819) 450-2923" + }, + { + "_id": "55d2fc86a8eb68257312e735", + "age": 33, + "name": "Guthrie Tyson", + "gender": "male", + "company": "GLUID", + "email": "guthrietyson@gluid.com", + "phone": "+1 (878) 496-3831" + }, + { + "_id": "55d2fc865d4f3b3777fc1573", + "age": 38, + "name": "Sellers Hodges", + "gender": "male", + "company": "BALOOBA", + "email": "sellershodges@balooba.com", + "phone": "+1 (895) 557-2331" + }, + { + "_id": "55d2fc860a91cf55298e2a24", + "age": 32, + "name": "Hawkins Hardin", + "gender": "male", + "company": "ZILLANET", + "email": "hawkinshardin@zillanet.com", + "phone": "+1 (852) 511-2796" + }, + { + "_id": "55d2fc867b1c618fcb9cb2c3", + "age": 26, + "name": "Bowman Buck", + "gender": "male", + "company": "APPLIDEC", + "email": "bowmanbuck@applidec.com", + "phone": "+1 (995) 500-2863" + }, + { + "_id": "55d2fc8666610d156551484b", + "age": 23, + "name": "Mcgee Delgado", + "gender": "male", + "company": "MANTRO", + "email": "mcgeedelgado@mantro.com", + "phone": "+1 (917) 490-2295" + }, + { + "_id": "55d2fc8685385c63f5a509b3", + "age": 24, + "name": "Petty Pena", + "gender": "male", + "company": "EXOSPEED", + "email": "pettypena@exospeed.com", + "phone": "+1 (929) 470-2022" + }, + { + "_id": "55d2fc864296df53bb778e52", + "age": 38, + "name": "Ray Mclaughlin", + "gender": "male", + "company": "PYRAMAX", + "email": "raymclaughlin@pyramax.com", + "phone": "+1 (935) 453-3720" + }, + { + "_id": "55d2fc86b157acc34692412b", + "age": 22, + "name": "Hopkins Wells", + "gender": "male", + "company": "NORALEX", + "email": "hopkinswells@noralex.com", + "phone": "+1 (986) 421-2293" + }, + { + "_id": "55d2fc861febd65bb3c91219", + "age": 38, + "name": "Patsy Strickland", + "gender": "female", + "company": "POLARIA", + "email": "patsystrickland@polaria.com", + "phone": "+1 (885) 408-2213" + }, + { + "_id": "55d2fc8693fbc24aa2bcc5a8", + "age": 31, + "name": "Wolf Delaney", + "gender": "male", + "company": "EXERTA", + "email": "wolfdelaney@exerta.com", + "phone": "+1 (969) 537-3201" + }, + { + "_id": "55d2fc86b923e0543d39fed4", + "age": 30, + "name": "Fulton Hewitt", + "gender": "male", + "company": "TWIGGERY", + "email": "fultonhewitt@twiggery.com", + "phone": "+1 (894) 483-2549" + }, + { + "_id": "55d2fc8672aff3d2369b2749", + "age": 40, + "name": "Nona Meadows", + "gender": "female", + "company": "ULTRIMAX", + "email": "nonameadows@ultrimax.com", + "phone": "+1 (997) 459-2012" + }, + { + "_id": "55d2fc86a3b6922e61cdcd72", + "age": 24, + "name": "Irwin Russo", + "gender": "male", + "company": "QUINTITY", + "email": "irwinrusso@quintity.com", + "phone": "+1 (985) 597-3841" + }, + { + "_id": "55d2fc86c28f4a90a41581b7", + "age": 34, + "name": "Mara Bowman", + "gender": "female", + "company": "ATOMICA", + "email": "marabowman@atomica.com", + "phone": "+1 (927) 578-2958" + }, + { + "_id": "55d2fc86ef827e1bbb5b3ceb", + "age": 40, + "name": "Leigh Schroeder", + "gender": "female", + "company": "ZIORE", + "email": "leighschroeder@ziore.com", + "phone": "+1 (963) 484-2519" + }, + { + "_id": "55d2fc86921329e8e044472a", + "age": 27, + "name": "Sweeney Riddle", + "gender": "male", + "company": "ELITA", + "email": "sweeneyriddle@elita.com", + "phone": "+1 (974) 536-2132" + }, + { + "_id": "55d2fc864b6067f7d828f1ba", + "age": 23, + "name": "Bell Kline", + "gender": "male", + "company": "ORBOID", + "email": "bellkline@orboid.com", + "phone": "+1 (827) 461-3466" + }, + { + "_id": "55d2fc86a541995fa67027ae", + "age": 20, + "name": "Morgan Aguirre", + "gender": "female", + "company": "AEORA", + "email": "morganaguirre@aeora.com", + "phone": "+1 (987) 494-2357" + }, + { + "_id": "55d2fc86af8d98e486bda0f5", + "age": 24, + "name": "Morrison Mcbride", + "gender": "male", + "company": "TECHMANIA", + "email": "morrisonmcbride@techmania.com", + "phone": "+1 (994) 470-2394" + }, + { + "_id": "55d2fc86371a2691da434cdd", + "age": 22, + "name": "Miles Salinas", + "gender": "male", + "company": "RODEOLOGY", + "email": "milessalinas@rodeology.com", + "phone": "+1 (898) 461-3008" + }, + { + "_id": "55d2fc865196aa926884d957", + "age": 36, + "name": "Lang Riggs", + "gender": "male", + "company": "PHOTOBIN", + "email": "langriggs@photobin.com", + "phone": "+1 (849) 503-2335" + }, + { + "_id": "55d2fc86b4e30cd686840e28", + "age": 30, + "name": "Kathy Phelps", + "gender": "female", + "company": "INTRAWEAR", + "email": "kathyphelps@intrawear.com", + "phone": "+1 (992) 499-2474" + }, + { + "_id": "55d2fc86f09accb089f91415", + "age": 24, + "name": "Moss Jimenez", + "gender": "male", + "company": "PROFLEX", + "email": "mossjimenez@proflex.com", + "phone": "+1 (842) 546-3491" + }, + { + "_id": "55d2fc86e3ba881a25584928", + "age": 20, + "name": "Moody Sexton", + "gender": "male", + "company": "CENTREGY", + "email": "moodysexton@centregy.com", + "phone": "+1 (856) 581-3293" + }, + { + "_id": "55d2fc861c85cf26c6d21a64", + "age": 31, + "name": "Nannie Price", + "gender": "female", + "company": "GEOSTELE", + "email": "nannieprice@geostele.com", + "phone": "+1 (936) 447-3486" + }, + { + "_id": "55d2fc86f1e23452254fda91", + "age": 32, + "name": "Summer Johnston", + "gender": "female", + "company": "EXTRAGENE", + "email": "summerjohnston@extragene.com", + "phone": "+1 (808) 508-2748" + }, + { + "_id": "55d2fc86940fcc0f17d5f213", + "age": 24, + "name": "Genevieve Lynch", + "gender": "female", + "company": "COREPAN", + "email": "genevievelynch@corepan.com", + "phone": "+1 (921) 532-2893" + }, + { + "_id": "55d2fc86cdf6056cd058466c", + "age": 23, + "name": "Stuart Oconnor", + "gender": "male", + "company": "PUSHCART", + "email": "stuartoconnor@pushcart.com", + "phone": "+1 (925) 515-3434" + }, + { + "_id": "55d2fc863bca896e3e2ac1a7", + "age": 21, + "name": "Adela Nieves", + "gender": "female", + "company": "MEDICROIX", + "email": "adelanieves@medicroix.com", + "phone": "+1 (910) 568-3916" + }, + { + "_id": "55d2fc86826d7c7fcfc2562e", + "age": 28, + "name": "Eaton Mcintosh", + "gender": "male", + "company": "MEGALL", + "email": "eatonmcintosh@megall.com", + "phone": "+1 (806) 440-2196" + }, + { + "_id": "55d2fc863d76c0458de8afb2", + "age": 35, + "name": "Sharron Hood", + "gender": "female", + "company": "UTARIAN", + "email": "sharronhood@utarian.com", + "phone": "+1 (824) 477-3364" + }, + { + "_id": "55d2fc86c0f317fe0cdd6f66", + "age": 24, + "name": "Allison Osborn", + "gender": "female", + "company": "TUBALUM", + "email": "allisonosborn@tubalum.com", + "phone": "+1 (913) 546-3966" + }, + { + "_id": "55d2fc86e1521a5a9896e0fa", + "age": 38, + "name": "Chelsea Jarvis", + "gender": "female", + "company": "SOPRANO", + "email": "chelseajarvis@soprano.com", + "phone": "+1 (834) 417-2904" + }, + { + "_id": "55d2fc86a5f4df72a31c9201", + "age": 22, + "name": "Finley Freeman", + "gender": "male", + "company": "COMBOT", + "email": "finleyfreeman@combot.com", + "phone": "+1 (886) 448-2820" + }, + { + "_id": "55d2fc86c4c2c783570c50eb", + "age": 31, + "name": "Lynn Miles", + "gender": "female", + "company": "ZENTIX", + "email": "lynnmiles@zentix.com", + "phone": "+1 (891) 450-2505" + }, + { + "_id": "55d2fc863e649b4a99367f40", + "age": 39, + "name": "Montoya Greene", + "gender": "male", + "company": "MEMORA", + "email": "montoyagreene@memora.com", + "phone": "+1 (809) 402-3541" + }, + { + "_id": "55d2fc86b82e96b2275df9f3", + "age": 28, + "name": "Castro Huff", + "gender": "male", + "company": "BLUPLANET", + "email": "castrohuff@bluplanet.com", + "phone": "+1 (966) 554-2469" + }, + { + "_id": "55d2fc86f4a8a7700a99e31c", + "age": 23, + "name": "Guadalupe Harmon", + "gender": "female", + "company": "ISOLOGIA", + "email": "guadalupeharmon@isologia.com", + "phone": "+1 (937) 497-2022" + }, + { + "_id": "55d2fc86551e7044f31f2520", + "age": 25, + "name": "Heidi Navarro", + "gender": "female", + "company": "POLARIUM", + "email": "heidinavarro@polarium.com", + "phone": "+1 (830) 493-3328" + }, + { + "_id": "55d2fc861ee43e4303351a4b", + "age": 25, + "name": "Fry Webster", + "gender": "male", + "company": "RENOVIZE", + "email": "frywebster@renovize.com", + "phone": "+1 (960) 600-3488" + }, + { + "_id": "55d2fc86e0d6778a1c6d7195", + "age": 35, + "name": "Candice Sharpe", + "gender": "female", + "company": "UNDERTAP", + "email": "candicesharpe@undertap.com", + "phone": "+1 (989) 436-2856" + }, + { + "_id": "55d2fc86267005541d225276", + "age": 25, + "name": "Vonda Hansen", + "gender": "female", + "company": "PROVIDCO", + "email": "vondahansen@providco.com", + "phone": "+1 (883) 484-2047" + }, + { + "_id": "55d2fc8605000bc9329d28e0", + "age": 21, + "name": "Lara Dominguez", + "gender": "male", + "company": "VIXO", + "email": "laradominguez@vixo.com", + "phone": "+1 (998) 440-3632" + }, + { + "_id": "55d2fc868b364833d935b192", + "age": 36, + "name": "Jillian Gibbs", + "gender": "female", + "company": "CUIZINE", + "email": "jilliangibbs@cuizine.com", + "phone": "+1 (996) 447-3083" + }, + { + "_id": "55d2fc863690a0784d2e8bc1", + "age": 35, + "name": "Frankie Cervantes", + "gender": "female", + "company": "ISODRIVE", + "email": "frankiecervantes@isodrive.com", + "phone": "+1 (895) 533-2371" + }, + { + "_id": "55d2fc86efb91d645101592f", + "age": 36, + "name": "Nieves Walton", + "gender": "male", + "company": "ZILPHUR", + "email": "nieveswalton@zilphur.com", + "phone": "+1 (866) 412-2377" + }, + { + "_id": "55d2fc86cba2fb0dfd9eb8c3", + "age": 38, + "name": "Celina Orr", + "gender": "female", + "company": "COMVEX", + "email": "celinaorr@comvex.com", + "phone": "+1 (960) 433-2380" + }, + { + "_id": "55d2fc8650eaf118ae048bce", + "age": 33, + "name": "Moreno Conway", + "gender": "male", + "company": "VIOCULAR", + "email": "morenoconway@viocular.com", + "phone": "+1 (808) 535-2624" + }, + { + "_id": "55d2fc860e41c77df0ea1151", + "age": 33, + "name": "Wilkerson Dodson", + "gender": "male", + "company": "COMTRAK", + "email": "wilkersondodson@comtrak.com", + "phone": "+1 (932) 427-2400" + }, + { + "_id": "55d2fc86175167613833c577", + "age": 21, + "name": "Crane Lloyd", + "gender": "male", + "company": "ARCHITAX", + "email": "cranelloyd@architax.com", + "phone": "+1 (984) 467-3498" + }, + { + "_id": "55d2fc863aa593013e09d04b", + "age": 25, + "name": "Marguerite Dorsey", + "gender": "female", + "company": "UNI", + "email": "margueritedorsey@uni.com", + "phone": "+1 (989) 558-2105" + }, + { + "_id": "55d2fc86248902726f0d1c53", + "age": 35, + "name": "Dillon William", + "gender": "male", + "company": "ROOFORIA", + "email": "dillonwilliam@rooforia.com", + "phone": "+1 (879) 600-3589" + }, + { + "_id": "55d2fc86caa1b7374cd83a72", + "age": 32, + "name": "Small Floyd", + "gender": "male", + "company": "ANACHO", + "email": "smallfloyd@anacho.com", + "phone": "+1 (906) 463-2357" + }, + { + "_id": "55d2fc86c086267c74616688", + "age": 32, + "name": "Dominique Horn", + "gender": "female", + "company": "ENTOGROK", + "email": "dominiquehorn@entogrok.com", + "phone": "+1 (871) 556-2943" + }, + { + "_id": "55d2fc86d3f7731c49d48fa7", + "age": 35, + "name": "Haley Shannon", + "gender": "female", + "company": "UNEEQ", + "email": "haleyshannon@uneeq.com", + "phone": "+1 (984) 471-3688" + }, + { + "_id": "55d2fc8678e81807871b2b13", + "age": 24, + "name": "Flowers Richards", + "gender": "male", + "company": "QUILITY", + "email": "flowersrichards@quility.com", + "phone": "+1 (956) 548-3667" + }, + { + "_id": "55d2fc86b4b462a2743c10d3", + "age": 33, + "name": "Hopper Rush", + "gender": "male", + "company": "VERBUS", + "email": "hopperrush@verbus.com", + "phone": "+1 (866) 578-2948" + }, + { + "_id": "55d2fc86d660e6f5bf9c1ce1", + "age": 31, + "name": "Randall Kelley", + "gender": "male", + "company": "INSURON", + "email": "randallkelley@insuron.com", + "phone": "+1 (922) 520-3464" + }, + { + "_id": "55d2fc86e70ad03e58c3e01f", + "age": 36, + "name": "Owens Drake", + "gender": "male", + "company": "FROLIX", + "email": "owensdrake@frolix.com", + "phone": "+1 (982) 516-3575" + }, + { + "_id": "55d2fc86ef78c8ad566afe4f", + "age": 28, + "name": "Irma Garrison", + "gender": "female", + "company": "EXOZENT", + "email": "irmagarrison@exozent.com", + "phone": "+1 (882) 536-2614" + }, + { + "_id": "55d2fc8675d6b4b33c5e482f", + "age": 34, + "name": "Baldwin Carver", + "gender": "male", + "company": "PHEAST", + "email": "baldwincarver@pheast.com", + "phone": "+1 (824) 521-2892" + }, + { + "_id": "55d2fc86d382b214a715305f", + "age": 23, + "name": "Short Harding", + "gender": "male", + "company": "LIQUICOM", + "email": "shortharding@liquicom.com", + "phone": "+1 (836) 578-2063" + }, + { + "_id": "55d2fc860420ee3ca95e2166", + "age": 35, + "name": "Christy Roberson", + "gender": "female", + "company": "BITREX", + "email": "christyroberson@bitrex.com", + "phone": "+1 (840) 426-2954" + }, + { + "_id": "55d2fc86dd67ce4e4f7b6d0c", + "age": 21, + "name": "Fern Knight", + "gender": "female", + "company": "ENTROPIX", + "email": "fernknight@entropix.com", + "phone": "+1 (944) 546-2456" + }, + { + "_id": "55d2fc8651510fb366709f12", + "age": 40, + "name": "Elva Taylor", + "gender": "female", + "company": "COMFIRM", + "email": "elvataylor@comfirm.com", + "phone": "+1 (834) 468-2999" + }, + { + "_id": "55d2fc869603515857919a7c", + "age": 26, + "name": "Carmen Bentley", + "gender": "female", + "company": "LIMAGE", + "email": "carmenbentley@limage.com", + "phone": "+1 (882) 425-3588" + }, + { + "_id": "55d2fc860991d6904808f8b3", + "age": 28, + "name": "Blair Dunn", + "gender": "male", + "company": "ESSENSIA", + "email": "blairdunn@essensia.com", + "phone": "+1 (896) 477-2617" + }, + { + "_id": "55d2fc862c99e4fefd17ab8d", + "age": 23, + "name": "Minerva Swanson", + "gender": "female", + "company": "EMTRAC", + "email": "minervaswanson@emtrac.com", + "phone": "+1 (905) 427-3942" + }, + { + "_id": "55d2fc86a2b4277d171e6ed6", + "age": 40, + "name": "Alicia Lawrence", + "gender": "female", + "company": "CONCILITY", + "email": "alicialawrence@concility.com", + "phone": "+1 (903) 452-2010" + }, + { + "_id": "55d2fc86f98eb74fe02a49f5", + "age": 33, + "name": "Bernice Fitzpatrick", + "gender": "female", + "company": "JASPER", + "email": "bernicefitzpatrick@jasper.com", + "phone": "+1 (987) 518-2248" + }, + { + "_id": "55d2fc86070af0b0109b6a4c", + "age": 20, + "name": "Wilder Blackburn", + "gender": "male", + "company": "ZENTILITY", + "email": "wilderblackburn@zentility.com", + "phone": "+1 (921) 548-2995" + }, + { + "_id": "55d2fc86249796ecd0b25829", + "age": 25, + "name": "Lambert Wilson", + "gender": "male", + "company": "ANIVET", + "email": "lambertwilson@anivet.com", + "phone": "+1 (920) 592-2261" + }, + { + "_id": "55d2fc863c6bdb691f62bdf4", + "age": 24, + "name": "Cook Lee", + "gender": "male", + "company": "ZILLADYNE", + "email": "cooklee@zilladyne.com", + "phone": "+1 (812) 566-2988" + }, + { + "_id": "55d2fc86cdeae66cf4fe6582", + "age": 29, + "name": "Karyn Horne", + "gender": "female", + "company": "OBLIQ", + "email": "karynhorne@obliq.com", + "phone": "+1 (909) 452-3599" + }, + { + "_id": "55d2fc86bcbabc9eb159e1e4", + "age": 23, + "name": "Rowe Stokes", + "gender": "male", + "company": "ANDERSHUN", + "email": "rowestokes@andershun.com", + "phone": "+1 (948) 531-2335" + }, + { + "_id": "55d2fc86a1b344b621d41baa", + "age": 27, + "name": "Gonzalez Merrill", + "gender": "male", + "company": "PLAYCE", + "email": "gonzalezmerrill@playce.com", + "phone": "+1 (989) 418-3057" + }, + { + "_id": "55d2fc86300074c184103b47", + "age": 33, + "name": "Kathie Preston", + "gender": "female", + "company": "LUMBREX", + "email": "kathiepreston@lumbrex.com", + "phone": "+1 (911) 586-3177" + }, + { + "_id": "55d2fc8646059e576c41ed1f", + "age": 26, + "name": "Antoinette Stevens", + "gender": "female", + "company": "APEXIA", + "email": "antoinettestevens@apexia.com", + "phone": "+1 (828) 597-3083" + }, + { + "_id": "55d2fc86b5ec5743baacc091", + "age": 22, + "name": "Schmidt Morton", + "gender": "male", + "company": "NORSUL", + "email": "schmidtmorton@norsul.com", + "phone": "+1 (821) 530-2454" + }, + { + "_id": "55d2fc86a521743243b01d2b", + "age": 37, + "name": "Joyner Wise", + "gender": "male", + "company": "MANGELICA", + "email": "joynerwise@mangelica.com", + "phone": "+1 (884) 448-3942" + }, + { + "_id": "55d2fc86d7c827b5ded4027f", + "age": 22, + "name": "Carpenter Carey", + "gender": "male", + "company": "CENTICE", + "email": "carpentercarey@centice.com", + "phone": "+1 (823) 585-2581" + }, + { + "_id": "55d2fc866866bf441f74abdf", + "age": 34, + "name": "Luz Hays", + "gender": "female", + "company": "KONGENE", + "email": "luzhays@kongene.com", + "phone": "+1 (999) 558-2282" + }, + { + "_id": "55d2fc86af95d619737eccb7", + "age": 27, + "name": "Lesley Frye", + "gender": "female", + "company": "SUREMAX", + "email": "lesleyfrye@suremax.com", + "phone": "+1 (982) 485-2811" + }, + { + "_id": "55d2fc86b41a0594552e4839", + "age": 31, + "name": "Jacqueline Ramsey", + "gender": "female", + "company": "LYRIA", + "email": "jacquelineramsey@lyria.com", + "phone": "+1 (961) 581-2500" + }, + { + "_id": "55d2fc8682aa54548863d4b1", + "age": 31, + "name": "Ina Ford", + "gender": "female", + "company": "ENERSOL", + "email": "inaford@enersol.com", + "phone": "+1 (895) 514-3441" + }, + { + "_id": "55d2fc866bc796d86ebd697b", + "age": 22, + "name": "Francisca Ashley", + "gender": "female", + "company": "SIGNIDYNE", + "email": "franciscaashley@signidyne.com", + "phone": "+1 (911) 566-2135" + }, + { + "_id": "55d2fc868ccc122c6a517033", + "age": 34, + "name": "Morton Owen", + "gender": "male", + "company": "SHOPABOUT", + "email": "mortonowen@shopabout.com", + "phone": "+1 (932) 576-3821" + }, + { + "_id": "55d2fc86751e661b56e6e3cd", + "age": 36, + "name": "Weber Manning", + "gender": "male", + "company": "CENTREE", + "email": "webermanning@centree.com", + "phone": "+1 (865) 582-3809" + }, + { + "_id": "55d2fc8620b9766f76797a13", + "age": 37, + "name": "Edwards Steele", + "gender": "male", + "company": "KATAKANA", + "email": "edwardssteele@katakana.com", + "phone": "+1 (959) 402-3657" + }, + { + "_id": "55d2fc860a13152e82b74839", + "age": 38, + "name": "Gabriela Boone", + "gender": "female", + "company": "OVOLO", + "email": "gabrielaboone@ovolo.com", + "phone": "+1 (910) 587-2744" + }, + { + "_id": "55d2fc864e9ca8a988600768", + "age": 32, + "name": "Tricia Guy", + "gender": "female", + "company": "TALKOLA", + "email": "triciaguy@talkola.com", + "phone": "+1 (960) 474-3508" + }, + { + "_id": "55d2fc86ce61b77671fd1f33", + "age": 32, + "name": "James Romero", + "gender": "female", + "company": "CANDECOR", + "email": "jamesromero@candecor.com", + "phone": "+1 (928) 478-3272" + }, + { + "_id": "55d2fc86f3cc105e20a44aa9", + "age": 25, + "name": "Casey Hammond", + "gender": "male", + "company": "PYRAMIA", + "email": "caseyhammond@pyramia.com", + "phone": "+1 (965) 539-2923" + }, + { + "_id": "55d2fc86429d859d9e54585f", + "age": 33, + "name": "Moran Wade", + "gender": "male", + "company": "BUGSALL", + "email": "moranwade@bugsall.com", + "phone": "+1 (996) 559-2965" + }, + { + "_id": "55d2fc86820b562eff651ebc", + "age": 34, + "name": "Earline Goff", + "gender": "female", + "company": "DATAGENE", + "email": "earlinegoff@datagene.com", + "phone": "+1 (976) 565-3513" + }, + { + "_id": "55d2fc865f6176e8301edcf3", + "age": 38, + "name": "Vickie Cherry", + "gender": "female", + "company": "SILODYNE", + "email": "vickiecherry@silodyne.com", + "phone": "+1 (943) 522-2438" + }, + { + "_id": "55d2fc8633cb19840995984c", + "age": 30, + "name": "Yates Avery", + "gender": "male", + "company": "DIGINETIC", + "email": "yatesavery@diginetic.com", + "phone": "+1 (813) 587-3611" + }, + { + "_id": "55d2fc8677d1a1c749d498eb", + "age": 24, + "name": "Hodges Langley", + "gender": "male", + "company": "QUOTEZART", + "email": "hodgeslangley@quotezart.com", + "phone": "+1 (857) 496-3905" + }, + { + "_id": "55d2fc86f49e5ceca6ff241d", + "age": 35, + "name": "Serrano Bartlett", + "gender": "male", + "company": "EXOSWITCH", + "email": "serranobartlett@exoswitch.com", + "phone": "+1 (809) 421-3677" + }, + { + "_id": "55d2fc86cd4921dcf0316691", + "age": 28, + "name": "Faye Wood", + "gender": "female", + "company": "EXOTERIC", + "email": "fayewood@exoteric.com", + "phone": "+1 (840) 417-2329" + }, + { + "_id": "55d2fc8697911f7ca9adfe37", + "age": 38, + "name": "Jamie Jennings", + "gender": "female", + "company": "SURETECH", + "email": "jamiejennings@suretech.com", + "phone": "+1 (945) 599-3768" + }, + { + "_id": "55d2fc8614e0225c06b40348", + "age": 34, + "name": "Levy Sellers", + "gender": "male", + "company": "STELAECOR", + "email": "levysellers@stelaecor.com", + "phone": "+1 (805) 519-2578" + }, + { + "_id": "55d2fc8635986d3994af8adc", + "age": 36, + "name": "Kelsey Montgomery", + "gender": "female", + "company": "IMANT", + "email": "kelseymontgomery@imant.com", + "phone": "+1 (894) 555-3420" + }, + { + "_id": "55d2fc8624dfa620d7b8a991", + "age": 33, + "name": "Rush Gates", + "gender": "male", + "company": "DIGIAL", + "email": "rushgates@digial.com", + "phone": "+1 (891) 477-2651" + }, + { + "_id": "55d2fc862d52fe806312e6dd", + "age": 40, + "name": "Holloway Gay", + "gender": "male", + "company": "EARTHMARK", + "email": "hollowaygay@earthmark.com", + "phone": "+1 (811) 540-3123" + }, + { + "_id": "55d2fc86a168313cea778ac0", + "age": 20, + "name": "Cotton Jackson", + "gender": "male", + "company": "ZOLARITY", + "email": "cottonjackson@zolarity.com", + "phone": "+1 (980) 445-3468" + }, + { + "_id": "55d2fc86577d2794043213ea", + "age": 38, + "name": "Lakeisha Blanchard", + "gender": "female", + "company": "SPEEDBOLT", + "email": "lakeishablanchard@speedbolt.com", + "phone": "+1 (839) 406-2400" + }, + { + "_id": "55d2fc8609e2760d5cc298f1", + "age": 39, + "name": "Alford Church", + "gender": "male", + "company": "GADTRON", + "email": "alfordchurch@gadtron.com", + "phone": "+1 (869) 400-2097" + }, + { + "_id": "55d2fc867e4a904c96668e08", + "age": 30, + "name": "Collins Carlson", + "gender": "male", + "company": "EVEREST", + "email": "collinscarlson@everest.com", + "phone": "+1 (944) 580-3905" + }, + { + "_id": "55d2fc86ddb3ea916627267a", + "age": 33, + "name": "Cain Hester", + "gender": "male", + "company": "IMMUNICS", + "email": "cainhester@immunics.com", + "phone": "+1 (899) 586-2934" + }, + { + "_id": "55d2fc86ffaa0a8952d1a400", + "age": 23, + "name": "Mia Baird", + "gender": "female", + "company": "NIPAZ", + "email": "miabaird@nipaz.com", + "phone": "+1 (940) 569-2850" + }, + { + "_id": "55d2fc865f259364ad5b4fff", + "age": 25, + "name": "Ramona Ewing", + "gender": "female", + "company": "COMTEST", + "email": "ramonaewing@comtest.com", + "phone": "+1 (879) 546-2754" + }, + { + "_id": "55d2fc862d6af75d6794bef5", + "age": 34, + "name": "Delacruz Goodwin", + "gender": "male", + "company": "SLOGANAUT", + "email": "delacruzgoodwin@sloganaut.com", + "phone": "+1 (928) 477-2688" + }, + { + "_id": "55d2fc86e34b19d6fd2ae261", + "age": 38, + "name": "Mcleod Moody", + "gender": "male", + "company": "ECRAZE", + "email": "mcleodmoody@ecraze.com", + "phone": "+1 (989) 598-3350" + }, + { + "_id": "55d2fc8608e305ebd55e0bac", + "age": 34, + "name": "Maddox Calhoun", + "gender": "male", + "company": "TELEPARK", + "email": "maddoxcalhoun@telepark.com", + "phone": "+1 (815) 593-3540" + }, + { + "_id": "55d2fc869e175ca83d7d6597", + "age": 36, + "name": "Cora Dale", + "gender": "female", + "company": "ZILLACTIC", + "email": "coradale@zillactic.com", + "phone": "+1 (866) 545-3632" + }, + { + "_id": "55d2fc861968b039322cb743", + "age": 27, + "name": "Knapp Miranda", + "gender": "male", + "company": "TOYLETRY", + "email": "knappmiranda@toyletry.com", + "phone": "+1 (835) 591-3111" + }, + { + "_id": "55d2fc86423f7b5a304d2175", + "age": 32, + "name": "Ida Petersen", + "gender": "female", + "company": "BILLMED", + "email": "idapetersen@billmed.com", + "phone": "+1 (898) 492-2148" + }, + { + "_id": "55d2fc862cfd92eb67375bba", + "age": 37, + "name": "Concepcion Wilcox", + "gender": "female", + "company": "MAXEMIA", + "email": "concepcionwilcox@maxemia.com", + "phone": "+1 (812) 516-2631" + }, + { + "_id": "55d2fc8695ffe246079f8f0c", + "age": 40, + "name": "Corine Daniel", + "gender": "female", + "company": "MEDCOM", + "email": "corinedaniel@medcom.com", + "phone": "+1 (991) 483-2257" + }, + { + "_id": "55d2fc861f1ff641b3aa7ee5", + "age": 31, + "name": "Latasha Byers", + "gender": "female", + "company": "RUGSTARS", + "email": "latashabyers@rugstars.com", + "phone": "+1 (817) 542-3231" + }, + { + "_id": "55d2fc86724fbfd025371582", + "age": 31, + "name": "Gayle Barrett", + "gender": "female", + "company": "PARAGONIA", + "email": "gaylebarrett@paragonia.com", + "phone": "+1 (870) 547-2454" + }, + { + "_id": "55d2fc869add6e70699650fa", + "age": 40, + "name": "Angelina Tyler", + "gender": "female", + "company": "VIRVA", + "email": "angelinatyler@virva.com", + "phone": "+1 (960) 425-3784" + }, + { + "_id": "55d2fc864c12d18424ac35be", + "age": 27, + "name": "Ratliff Franks", + "gender": "male", + "company": "CEMENTION", + "email": "ratlifffranks@cemention.com", + "phone": "+1 (958) 424-2396" + }, + { + "_id": "55d2fc86bad0be82f6e2f83b", + "age": 40, + "name": "Landry Zimmerman", + "gender": "male", + "company": "JETSILK", + "email": "landryzimmerman@jetsilk.com", + "phone": "+1 (947) 573-2755" + }, + { + "_id": "55d2fc86b56ff40ff0ed70e3", + "age": 23, + "name": "Greta West", + "gender": "female", + "company": "UBERLUX", + "email": "gretawest@uberlux.com", + "phone": "+1 (995) 542-3886" + }, + { + "_id": "55d2fc8667dfd03049bf08eb", + "age": 30, + "name": "Camacho Nelson", + "gender": "male", + "company": "KYAGORO", + "email": "camachonelson@kyagoro.com", + "phone": "+1 (881) 500-3970" + }, + { + "_id": "55d2fc865458ecf3d2995a63", + "age": 36, + "name": "June Turner", + "gender": "female", + "company": "RECOGNIA", + "email": "juneturner@recognia.com", + "phone": "+1 (976) 466-2777" + }, + { + "_id": "55d2fc8682f4304f0889c829", + "age": 31, + "name": "Mckinney Stark", + "gender": "male", + "company": "OPTICOM", + "email": "mckinneystark@opticom.com", + "phone": "+1 (951) 500-3946" + }, + { + "_id": "55d2fc86ff8211995849d831", + "age": 20, + "name": "Hammond Fletcher", + "gender": "male", + "company": "ERSUM", + "email": "hammondfletcher@ersum.com", + "phone": "+1 (974) 541-3273" + }, + { + "_id": "55d2fc86a9792a08912bdb8e", + "age": 23, + "name": "White Fischer", + "gender": "male", + "company": "REALMO", + "email": "whitefischer@realmo.com", + "phone": "+1 (963) 533-2428" + }, + { + "_id": "55d2fc86c5ba8287455030be", + "age": 36, + "name": "Kimberly Mcguire", + "gender": "female", + "company": "TETAK", + "email": "kimberlymcguire@tetak.com", + "phone": "+1 (846) 410-3414" + }, + { + "_id": "55d2fc862a665bca942115f3", + "age": 35, + "name": "Sara Hurst", + "gender": "female", + "company": "FORTEAN", + "email": "sarahurst@fortean.com", + "phone": "+1 (857) 530-3627" + }, + { + "_id": "55d2fc86b72bee240f10055e", + "age": 29, + "name": "Chandler English", + "gender": "male", + "company": "PAPRICUT", + "email": "chandlerenglish@papricut.com", + "phone": "+1 (978) 582-2348" + }, + { + "_id": "55d2fc861c2bccedeac29892", + "age": 29, + "name": "Waters Riley", + "gender": "male", + "company": "ECSTASIA", + "email": "watersriley@ecstasia.com", + "phone": "+1 (842) 579-3426" + }, + { + "_id": "55d2fc86526f36994de2038a", + "age": 34, + "name": "Wood Gomez", + "gender": "male", + "company": "ASSITIA", + "email": "woodgomez@assitia.com", + "phone": "+1 (954) 565-2413" + }, + { + "_id": "55d2fc86bec8fe1e60977c9c", + "age": 31, + "name": "Fields Decker", + "gender": "male", + "company": "EMERGENT", + "email": "fieldsdecker@emergent.com", + "phone": "+1 (992) 489-3712" + }, + { + "_id": "55d2fc868b903951ddd71703", + "age": 28, + "name": "Barry Woods", + "gender": "male", + "company": "OZEAN", + "email": "barrywoods@ozean.com", + "phone": "+1 (885) 433-3285" + }, + { + "_id": "55d2fc86ca3984177237f17e", + "age": 29, + "name": "Whitfield Higgins", + "gender": "male", + "company": "PEARLESEX", + "email": "whitfieldhiggins@pearlesex.com", + "phone": "+1 (869) 468-3186" + }, + { + "_id": "55d2fc8678dd83bf6cc16648", + "age": 27, + "name": "Haynes Mills", + "gender": "male", + "company": "ZOARERE", + "email": "haynesmills@zoarere.com", + "phone": "+1 (886) 576-3206" + }, + { + "_id": "55d2fc86b6ba36d7927b8765", + "age": 27, + "name": "Kellie Hurley", + "gender": "female", + "company": "GYNKO", + "email": "kelliehurley@gynko.com", + "phone": "+1 (844) 548-2894" + }, + { + "_id": "55d2fc869adb64b23212bdfc", + "age": 30, + "name": "Brandi Shields", + "gender": "female", + "company": "KENGEN", + "email": "brandishields@kengen.com", + "phone": "+1 (947) 447-3081" + }, + { + "_id": "55d2fc8617e038558bbd0e5f", + "age": 22, + "name": "Malinda Gordon", + "gender": "female", + "company": "QUILCH", + "email": "malindagordon@quilch.com", + "phone": "+1 (945) 466-2414" + }, + { + "_id": "55d2fc8614756992c50aabfd", + "age": 30, + "name": "Wooten Mcknight", + "gender": "male", + "company": "APPLIDECK", + "email": "wootenmcknight@applideck.com", + "phone": "+1 (994) 416-2156" + }, + { + "_id": "55d2fc86b44fbdb8c8ea3a67", + "age": 36, + "name": "Mona Thomas", + "gender": "female", + "company": "KROG", + "email": "monathomas@krog.com", + "phone": "+1 (924) 423-3381" + }, + { + "_id": "55d2fc86ded545b6b4f5e536", + "age": 22, + "name": "Bates Cole", + "gender": "male", + "company": "DIGIRANG", + "email": "batescole@digirang.com", + "phone": "+1 (956) 409-2471" + }, + { + "_id": "55d2fc864abd3a5951c8e07c", + "age": 33, + "name": "Shirley Potts", + "gender": "female", + "company": "OMATOM", + "email": "shirleypotts@omatom.com", + "phone": "+1 (804) 496-2921" + }, + { + "_id": "55d2fc864e3992c902987b9c", + "age": 22, + "name": "Adrian Branch", + "gender": "female", + "company": "MULTIFLEX", + "email": "adrianbranch@multiflex.com", + "phone": "+1 (817) 499-3955" + }, + { + "_id": "55d2fc8679037ccc9d0d84d0", + "age": 25, + "name": "Deanne Rosa", + "gender": "female", + "company": "QUONATA", + "email": "deannerosa@quonata.com", + "phone": "+1 (896) 463-2190" + }, + { + "_id": "55d2fc869ec47f3f9745bcbc", + "age": 28, + "name": "Sherrie Bowers", + "gender": "female", + "company": "GOLISTIC", + "email": "sherriebowers@golistic.com", + "phone": "+1 (854) 539-3836" + }, + { + "_id": "55d2fc86c953130389da55f9", + "age": 21, + "name": "Sharp Douglas", + "gender": "male", + "company": "CHILLIUM", + "email": "sharpdouglas@chillium.com", + "phone": "+1 (999) 513-3550" + }, + { + "_id": "55d2fc86d42d710fef2a7781", + "age": 22, + "name": "Sandy Dillard", + "gender": "female", + "company": "PHARMACON", + "email": "sandydillard@pharmacon.com", + "phone": "+1 (844) 433-2832" + }, + { + "_id": "55d2fc86aa4130e6998a333b", + "age": 20, + "name": "Naomi Willis", + "gender": "female", + "company": "SAVVY", + "email": "naomiwillis@savvy.com", + "phone": "+1 (826) 499-3221" + }, + { + "_id": "55d2fc869c2c97145040281b", + "age": 38, + "name": "Rivera Stone", + "gender": "male", + "company": "ORBIXTAR", + "email": "riverastone@orbixtar.com", + "phone": "+1 (994) 439-3810" + }, + { + "_id": "55d2fc86e7165c13cd5905c1", + "age": 22, + "name": "Oliver Day", + "gender": "male", + "company": "PORTALIS", + "email": "oliverday@portalis.com", + "phone": "+1 (844) 464-2363" + }, + { + "_id": "55d2fc86b6b619b4d04c7640", + "age": 39, + "name": "Rachael Owens", + "gender": "female", + "company": "NURALI", + "email": "rachaelowens@nurali.com", + "phone": "+1 (856) 418-3617" + }, + { + "_id": "55d2fc865f2612144e6f27e6", + "age": 30, + "name": "Winifred Molina", + "gender": "female", + "company": "NITRACYR", + "email": "winifredmolina@nitracyr.com", + "phone": "+1 (881) 417-3559" + }, + { + "_id": "55d2fc86c38ab2f341eb9717", + "age": 33, + "name": "Helen Callahan", + "gender": "female", + "company": "BOLAX", + "email": "helencallahan@bolax.com", + "phone": "+1 (929) 407-3095" + }, + { + "_id": "55d2fc86e6aa094f47df5373", + "age": 32, + "name": "Leblanc Christensen", + "gender": "male", + "company": "LIQUIDOC", + "email": "leblancchristensen@liquidoc.com", + "phone": "+1 (878) 568-2054" + }, + { + "_id": "55d2fc86b297912d153c4e8f", + "age": 31, + "name": "Hill Robbins", + "gender": "male", + "company": "QUANTALIA", + "email": "hillrobbins@quantalia.com", + "phone": "+1 (826) 430-2750" + }, + { + "_id": "55d2fc86e776e4075d7df74e", + "age": 36, + "name": "Tabitha Whitley", + "gender": "female", + "company": "ZILLIDIUM", + "email": "tabithawhitley@zillidium.com", + "phone": "+1 (838) 516-3637" + }, + { + "_id": "55d2fc86197a382bbf34e81f", + "age": 36, + "name": "May Pearson", + "gender": "male", + "company": "RODEOMAD", + "email": "maypearson@rodeomad.com", + "phone": "+1 (854) 429-3462" + }, + { + "_id": "55d2fc863ade7d3517aed2c6", + "age": 28, + "name": "Alvarez Austin", + "gender": "male", + "company": "CUBIX", + "email": "alvarezaustin@cubix.com", + "phone": "+1 (847) 594-3735" + }, + { + "_id": "55d2fc86b158d5d260362ac4", + "age": 31, + "name": "Misty Shepard", + "gender": "female", + "company": "COMVEYER", + "email": "mistyshepard@comveyer.com", + "phone": "+1 (901) 567-3881" + }, + { + "_id": "55d2fc8646bce5646a0b5258", + "age": 22, + "name": "Yvette Hensley", + "gender": "female", + "company": "MYOPIUM", + "email": "yvettehensley@myopium.com", + "phone": "+1 (890) 456-2157" + }, + { + "_id": "55d2fc86c362f848f08340c2", + "age": 36, + "name": "Hernandez Rowe", + "gender": "male", + "company": "EARTHPLEX", + "email": "hernandezrowe@earthplex.com", + "phone": "+1 (807) 502-2308" + }, + { + "_id": "55d2fc8640a621a6f035ce8d", + "age": 39, + "name": "Maura Harper", + "gender": "female", + "company": "ROBOID", + "email": "mauraharper@roboid.com", + "phone": "+1 (927) 506-2290" + }, + { + "_id": "55d2fc86965968be6314d56f", + "age": 20, + "name": "Luisa Gardner", + "gender": "female", + "company": "WAAB", + "email": "luisagardner@waab.com", + "phone": "+1 (964) 514-2189" + }, + { + "_id": "55d2fc86e12b5b70fffd430a", + "age": 36, + "name": "Christa Bradley", + "gender": "female", + "company": "FURNIGEER", + "email": "christabradley@furnigeer.com", + "phone": "+1 (871) 587-3404" + }, + { + "_id": "55d2fc86153d11b40a9bf8bc", + "age": 26, + "name": "Murphy Fleming", + "gender": "male", + "company": "COLLAIRE", + "email": "murphyfleming@collaire.com", + "phone": "+1 (909) 598-3130" + }, + { + "_id": "55d2fc863ec1e4fc29e5ce50", + "age": 28, + "name": "Bobbi Harrington", + "gender": "female", + "company": "MEDIFAX", + "email": "bobbiharrington@medifax.com", + "phone": "+1 (825) 598-2607" + }, + { + "_id": "55d2fc86f4b17262ea0129c6", + "age": 40, + "name": "Paige Flynn", + "gender": "female", + "company": "PULZE", + "email": "paigeflynn@pulze.com", + "phone": "+1 (956) 529-3295" + }, + { + "_id": "55d2fc861c8dc17cb598e70d", + "age": 21, + "name": "Nina Moon", + "gender": "female", + "company": "ZOLAVO", + "email": "ninamoon@zolavo.com", + "phone": "+1 (863) 540-3993" + }, + { + "_id": "55d2fc869d8ee9d95ab9fee4", + "age": 36, + "name": "Shauna Mckay", + "gender": "female", + "company": "LUNCHPOD", + "email": "shaunamckay@lunchpod.com", + "phone": "+1 (879) 435-3179" + }, + { + "_id": "55d2fc86a03f430c5e56194b", + "age": 36, + "name": "Amie Nicholson", + "gender": "female", + "company": "LETPRO", + "email": "amienicholson@letpro.com", + "phone": "+1 (839) 600-3014" + }, + { + "_id": "55d2fc86062b4615153832f6", + "age": 31, + "name": "Pennington Whitney", + "gender": "male", + "company": "TRI@TRIBALOG", + "email": "penningtonwhitney@tri@tribalog.com", + "phone": "+1 (950) 487-3727" + }, + { + "_id": "55d2fc868849f1c7f4f80541", + "age": 23, + "name": "Gena Barton", + "gender": "female", + "company": "VORTEXACO", + "email": "genabarton@vortexaco.com", + "phone": "+1 (889) 515-2172" + }, + { + "_id": "55d2fc860c13e786f86024fd", + "age": 27, + "name": "Ashley Stephens", + "gender": "female", + "company": "ZENSUS", + "email": "ashleystephens@zensus.com", + "phone": "+1 (949) 525-3726" + }, + { + "_id": "55d2fc86cca3638bdc9c942c", + "age": 38, + "name": "Cherie Morgan", + "gender": "female", + "company": "HELIXO", + "email": "cheriemorgan@helixo.com", + "phone": "+1 (815) 514-2167" + }, + { + "_id": "55d2fc8630d769398c9a0788", + "age": 31, + "name": "Ann Wiggins", + "gender": "female", + "company": "NIXELT", + "email": "annwiggins@nixelt.com", + "phone": "+1 (878) 567-2808" + }, + { + "_id": "55d2fc86f3d0744abd99ee4a", + "age": 36, + "name": "Hinton Keller", + "gender": "male", + "company": "VENOFLEX", + "email": "hintonkeller@venoflex.com", + "phone": "+1 (978) 499-2652" + }, + { + "_id": "55d2fc86714b2f2f7f59ff69", + "age": 32, + "name": "Marsh Mullins", + "gender": "male", + "company": "ZIALACTIC", + "email": "marshmullins@zialactic.com", + "phone": "+1 (908) 537-2112" + }, + { + "_id": "55d2fc8644c73b5870be171c", + "age": 28, + "name": "Holland Underwood", + "gender": "male", + "company": "ZILLACON", + "email": "hollandunderwood@zillacon.com", + "phone": "+1 (968) 454-2162" + }, + { + "_id": "55d2fc8648b8691a6a646f9e", + "age": 31, + "name": "Beverly Oneal", + "gender": "female", + "company": "BYTREX", + "email": "beverlyoneal@bytrex.com", + "phone": "+1 (969) 522-2598" + }, + { + "_id": "55d2fc86b3e9627aa4f5f88a", + "age": 24, + "name": "Leanne Frazier", + "gender": "female", + "company": "HOPELI", + "email": "leannefrazier@hopeli.com", + "phone": "+1 (923) 532-3379" + }, + { + "_id": "55d2fc867bdc2935055e4595", + "age": 31, + "name": "Rhodes Cash", + "gender": "male", + "company": "PAPRIKUT", + "email": "rhodescash@paprikut.com", + "phone": "+1 (830) 507-2776" + }, + { + "_id": "55d2fc86543e21b2bc15201d", + "age": 30, + "name": "Cherry Bush", + "gender": "male", + "company": "PROGENEX", + "email": "cherrybush@progenex.com", + "phone": "+1 (935) 577-2984" + }, + { + "_id": "55d2fc8660c1a32dfdf4fe67", + "age": 32, + "name": "Jacobs Clark", + "gender": "male", + "company": "COMDOM", + "email": "jacobsclark@comdom.com", + "phone": "+1 (947) 434-2665" + }, + { + "_id": "55d2fc861641831257904d9c", + "age": 37, + "name": "Nell Mcmahon", + "gender": "female", + "company": "SLAMBDA", + "email": "nellmcmahon@slambda.com", + "phone": "+1 (831) 462-2693" + }, + { + "_id": "55d2fc86835340478ec889e2", + "age": 37, + "name": "Palmer Livingston", + "gender": "male", + "company": "DIGIGEN", + "email": "palmerlivingston@digigen.com", + "phone": "+1 (817) 443-2049" + }, + { + "_id": "55d2fc8697f9fd666529fa34", + "age": 40, + "name": "Ayala Schmidt", + "gender": "male", + "company": "EWAVES", + "email": "ayalaschmidt@ewaves.com", + "phone": "+1 (899) 576-2845" + }, + { + "_id": "55d2fc8682936b03c7c044de", + "age": 26, + "name": "Lynch Beck", + "gender": "male", + "company": "INDEXIA", + "email": "lynchbeck@indexia.com", + "phone": "+1 (942) 411-3724" + }, + { + "_id": "55d2fc86bb74729fe35b2bcc", + "age": 20, + "name": "Yang Hickman", + "gender": "male", + "company": "UXMOX", + "email": "yanghickman@uxmox.com", + "phone": "+1 (944) 554-2948" + }, + { + "_id": "55d2fc86bb04a1e5e39143b1", + "age": 30, + "name": "Andrews Lucas", + "gender": "male", + "company": "CIPROMOX", + "email": "andrewslucas@cipromox.com", + "phone": "+1 (942) 401-2756" + }, + { + "_id": "55d2fc86b862c1492a4c5bc1", + "age": 34, + "name": "Rosa Valdez", + "gender": "female", + "company": "ONTALITY", + "email": "rosavaldez@ontality.com", + "phone": "+1 (963) 414-3056" + }, + { + "_id": "55d2fc868ab9f2f25a46a850", + "age": 30, + "name": "Maria Caldwell", + "gender": "female", + "company": "ACRODANCE", + "email": "mariacaldwell@acrodance.com", + "phone": "+1 (963) 433-2398" + }, + { + "_id": "55d2fc868d206b4d99f1f0b2", + "age": 36, + "name": "Gilda Chase", + "gender": "female", + "company": "KOFFEE", + "email": "gildachase@koffee.com", + "phone": "+1 (980) 591-3955" + }, + { + "_id": "55d2fc86e0c5a2f031b4a0d9", + "age": 24, + "name": "Dejesus Pittman", + "gender": "male", + "company": "SLAX", + "email": "dejesuspittman@slax.com", + "phone": "+1 (819) 574-2826" + }, + { + "_id": "55d2fc868190178a9af16ec5", + "age": 23, + "name": "Valdez Gibson", + "gender": "male", + "company": "ELECTONIC", + "email": "valdezgibson@electonic.com", + "phone": "+1 (809) 520-3985" + }, + { + "_id": "55d2fc86d3bc3cf86e16bc5b", + "age": 21, + "name": "Aguilar Bird", + "gender": "male", + "company": "ULTRASURE", + "email": "aguilarbird@ultrasure.com", + "phone": "+1 (813) 455-3814" + }, + { + "_id": "55d2fc86701c49cc235f0b49", + "age": 24, + "name": "Bentley Mooney", + "gender": "male", + "company": "BEDLAM", + "email": "bentleymooney@bedlam.com", + "phone": "+1 (870) 530-2188" + }, + { + "_id": "55d2fc863fe6d9fc492a1ca5", + "age": 28, + "name": "Ruby Wooten", + "gender": "female", + "company": "MARKETOID", + "email": "rubywooten@marketoid.com", + "phone": "+1 (813) 470-3521" + }, + { + "_id": "55d2fc8622f489f721743001", + "age": 28, + "name": "Garrison Blevins", + "gender": "male", + "company": "KEENGEN", + "email": "garrisonblevins@keengen.com", + "phone": "+1 (974) 538-2989" + }, + { + "_id": "55d2fc863dc60d226c55ece7", + "age": 33, + "name": "Harper Tanner", + "gender": "male", + "company": "QABOOS", + "email": "harpertanner@qaboos.com", + "phone": "+1 (953) 406-3082" + }, + { + "_id": "55d2fc86fecd601439c2702e", + "age": 32, + "name": "Best Robles", + "gender": "male", + "company": "OCEANICA", + "email": "bestrobles@oceanica.com", + "phone": "+1 (815) 539-3097" + }, + { + "_id": "55d2fc86ba25530a2149beab", + "age": 36, + "name": "Marian Bradshaw", + "gender": "female", + "company": "XYQAG", + "email": "marianbradshaw@xyqag.com", + "phone": "+1 (928) 410-3218" + }, + { + "_id": "55d2fc867352b6b799d365e4", + "age": 23, + "name": "Whitley Oneil", + "gender": "male", + "company": "XURBAN", + "email": "whitleyoneil@xurban.com", + "phone": "+1 (802) 578-3671" + }, + { + "_id": "55d2fc865ee137dbdee5cde2", + "age": 34, + "name": "Ella Fox", + "gender": "female", + "company": "TUBESYS", + "email": "ellafox@tubesys.com", + "phone": "+1 (920) 524-3066" + }, + { + "_id": "55d2fc860cc159486a822879", + "age": 30, + "name": "Farmer Castro", + "gender": "male", + "company": "QNEKT", + "email": "farmercastro@qnekt.com", + "phone": "+1 (866) 578-2968" + }, + { + "_id": "55d2fc863b57eefc3015d373", + "age": 28, + "name": "Guy Cochran", + "gender": "male", + "company": "VICON", + "email": "guycochran@vicon.com", + "phone": "+1 (840) 567-2191" + }, + { + "_id": "55d2fc863df7dfda22e99029", + "age": 32, + "name": "Leach Rocha", + "gender": "male", + "company": "DANJA", + "email": "leachrocha@danja.com", + "phone": "+1 (971) 589-3164" + }, + { + "_id": "55d2fc86aba3b9d7ce3f877c", + "age": 36, + "name": "Tanner Hayes", + "gender": "male", + "company": "TELEQUIET", + "email": "tannerhayes@telequiet.com", + "phone": "+1 (813) 526-2989" + }, + { + "_id": "55d2fc862cd6fb84f734fa0e", + "age": 30, + "name": "Keith Maldonado", + "gender": "male", + "company": "MAGNEATO", + "email": "keithmaldonado@magneato.com", + "phone": "+1 (997) 419-3200" + }, + { + "_id": "55d2fc8663d4dc1e43943f62", + "age": 29, + "name": "Winnie Harrell", + "gender": "female", + "company": "FRENEX", + "email": "winnieharrell@frenex.com", + "phone": "+1 (966) 565-2447" + }, + { + "_id": "55d2fc86b57f9312b0d28a1d", + "age": 28, + "name": "Sandoval Garza", + "gender": "male", + "company": "INTERLOO", + "email": "sandovalgarza@interloo.com", + "phone": "+1 (972) 597-3431" + }, + { + "_id": "55d2fc86a356d194d285d160", + "age": 23, + "name": "Lina Dejesus", + "gender": "female", + "company": "ORONOKO", + "email": "linadejesus@oronoko.com", + "phone": "+1 (910) 560-2515" + }, + { + "_id": "55d2fc862f4cd754495f93c7", + "age": 30, + "name": "Jana Spence", + "gender": "female", + "company": "ZILLACOM", + "email": "janaspence@zillacom.com", + "phone": "+1 (994) 436-2023" + }, + { + "_id": "55d2fc869b25329fae4936d0", + "age": 32, + "name": "Mcdowell Fisher", + "gender": "male", + "company": "GYNK", + "email": "mcdowellfisher@gynk.com", + "phone": "+1 (941) 587-3569" + }, + { + "_id": "55d2fc866b52b90a3758bdd3", + "age": 28, + "name": "Farley Bernard", + "gender": "male", + "company": "NETROPIC", + "email": "farleybernard@netropic.com", + "phone": "+1 (856) 540-2658" + }, + { + "_id": "55d2fc864086901eaeb80443", + "age": 25, + "name": "Lorna Howe", + "gender": "female", + "company": "ISOSWITCH", + "email": "lornahowe@isoswitch.com", + "phone": "+1 (851) 432-3160" + }, + { + "_id": "55d2fc86b4ac38891f11340b", + "age": 25, + "name": "English Watts", + "gender": "male", + "company": "INFOTRIPS", + "email": "englishwatts@infotrips.com", + "phone": "+1 (942) 481-2578" + }, + { + "_id": "55d2fc86e0d047d4eb3c224f", + "age": 35, + "name": "Burch Howell", + "gender": "male", + "company": "FANFARE", + "email": "burchhowell@fanfare.com", + "phone": "+1 (986) 507-2725" + }, + { + "_id": "55d2fc86330a8dab2ddbc0c4", + "age": 39, + "name": "Hudson Bender", + "gender": "male", + "company": "ENORMO", + "email": "hudsonbender@enormo.com", + "phone": "+1 (982) 553-3993" + }, + { + "_id": "55d2fc8600bb27f4ba215be8", + "age": 30, + "name": "Mcdonald Whitehead", + "gender": "male", + "company": "SENMAO", + "email": "mcdonaldwhitehead@senmao.com", + "phone": "+1 (837) 449-3264" + }, + { + "_id": "55d2fc8683787d8b7b400408", + "age": 31, + "name": "Hope Holden", + "gender": "female", + "company": "EVENTIX", + "email": "hopeholden@eventix.com", + "phone": "+1 (888) 436-2921" + }, + { + "_id": "55d2fc86cd7d2a5c962d2c05", + "age": 36, + "name": "Suarez Mejia", + "gender": "male", + "company": "BUZZWORKS", + "email": "suarezmejia@buzzworks.com", + "phone": "+1 (919) 526-3966" + }, + { + "_id": "55d2fc8612bf58c9b2d953cf", + "age": 27, + "name": "Michele Little", + "gender": "female", + "company": "VINCH", + "email": "michelelittle@vinch.com", + "phone": "+1 (817) 414-2165" + }, + { + "_id": "55d2fc864a8103126f905972", + "age": 25, + "name": "Patrick Cooke", + "gender": "male", + "company": "BEDDER", + "email": "patrickcooke@bedder.com", + "phone": "+1 (993) 587-2086" + }, + { + "_id": "55d2fc865ddf784cac1f023c", + "age": 31, + "name": "Holcomb Beasley", + "gender": "male", + "company": "TECHTRIX", + "email": "holcombbeasley@techtrix.com", + "phone": "+1 (879) 458-3507" + }, + { + "_id": "55d2fc86e27a28e9e9b0d232", + "age": 34, + "name": "Catalina Donovan", + "gender": "female", + "company": "FILODYNE", + "email": "catalinadonovan@filodyne.com", + "phone": "+1 (818) 542-2296" + }, + { + "_id": "55d2fc861801cbac57fa3186", + "age": 21, + "name": "Leslie Bryan", + "gender": "female", + "company": "LUXURIA", + "email": "lesliebryan@luxuria.com", + "phone": "+1 (917) 590-3272" + }, + { + "_id": "55d2fc86d3aa760444ec40cc", + "age": 37, + "name": "Hobbs Noel", + "gender": "male", + "company": "ZILLA", + "email": "hobbsnoel@zilla.com", + "phone": "+1 (917) 430-3792" + }, + { + "_id": "55d2fc86f53d0267e33ddb67", + "age": 38, + "name": "Nunez Meyers", + "gender": "male", + "company": "ENTROFLEX", + "email": "nunezmeyers@entroflex.com", + "phone": "+1 (940) 419-3943" + }, + { + "_id": "55d2fc867805cf4262e648a9", + "age": 37, + "name": "Sonya Sloan", + "gender": "female", + "company": "SURELOGIC", + "email": "sonyasloan@surelogic.com", + "phone": "+1 (924) 561-3268" + }, + { + "_id": "55d2fc86a3b756eaaef9f9fa", + "age": 31, + "name": "Angeline Sargent", + "gender": "female", + "company": "QUIZMO", + "email": "angelinesargent@quizmo.com", + "phone": "+1 (952) 539-3859" + }, + { + "_id": "55d2fc860f0e4be242c866a3", + "age": 40, + "name": "Norris Webb", + "gender": "male", + "company": "ZENCO", + "email": "norriswebb@zenco.com", + "phone": "+1 (834) 527-2399" + }, + { + "_id": "55d2fc86b2da030fb755d74c", + "age": 38, + "name": "Wise Bonner", + "gender": "male", + "company": "KINETICA", + "email": "wisebonner@kinetica.com", + "phone": "+1 (938) 416-3537" + }, + { + "_id": "55d2fc8699036f35ee214843", + "age": 25, + "name": "Imogene Blankenship", + "gender": "female", + "company": "POLARAX", + "email": "imogeneblankenship@polarax.com", + "phone": "+1 (877) 476-3735" + }, + { + "_id": "55d2fc86009b5a1658986a92", + "age": 27, + "name": "Silva Schneider", + "gender": "male", + "company": "MINGA", + "email": "silvaschneider@minga.com", + "phone": "+1 (884) 420-2111" + }, + { + "_id": "55d2fc86e3dcb6d4996e9813", + "age": 40, + "name": "Lawanda Cortez", + "gender": "female", + "company": "HOMETOWN", + "email": "lawandacortez@hometown.com", + "phone": "+1 (946) 525-3826" + }, + { + "_id": "55d2fc8641977ef422e73176", + "age": 40, + "name": "Clements Waters", + "gender": "male", + "company": "FLEETMIX", + "email": "clementswaters@fleetmix.com", + "phone": "+1 (973) 523-2395" + }, + { + "_id": "55d2fc869d0f5363ad055935", + "age": 20, + "name": "Ofelia Gilbert", + "gender": "female", + "company": "ECRATER", + "email": "ofeliagilbert@ecrater.com", + "phone": "+1 (828) 404-2646" + }, + { + "_id": "55d2fc86c39b876162269895", + "age": 23, + "name": "Valenzuela Carney", + "gender": "male", + "company": "HYDROCOM", + "email": "valenzuelacarney@hydrocom.com", + "phone": "+1 (842) 566-3650" + }, + { + "_id": "55d2fc86546c31933b02dd85", + "age": 28, + "name": "Wells Santana", + "gender": "male", + "company": "ZIDOX", + "email": "wellssantana@zidox.com", + "phone": "+1 (886) 527-2963" + }, + { + "_id": "55d2fc86a981c3741ad50f8c", + "age": 29, + "name": "Karla Carroll", + "gender": "female", + "company": "MAGNEMO", + "email": "karlacarroll@magnemo.com", + "phone": "+1 (922) 418-3361" + }, + { + "_id": "55d2fc861054327ef76378e3", + "age": 29, + "name": "Juliet Butler", + "gender": "female", + "company": "ORBAXTER", + "email": "julietbutler@orbaxter.com", + "phone": "+1 (838) 554-2269" + }, + { + "_id": "55d2fc868120c8a8e4eb9149", + "age": 26, + "name": "Lisa Copeland", + "gender": "female", + "company": "SOFTMICRO", + "email": "lisacopeland@softmicro.com", + "phone": "+1 (915) 577-2302" + }, + { + "_id": "55d2fc86b0eb908d67787f43", + "age": 37, + "name": "Mcmahon Spencer", + "gender": "male", + "company": "BOILCAT", + "email": "mcmahonspencer@boilcat.com", + "phone": "+1 (871) 501-2558" + }, + { + "_id": "55d2fc860c0b6f5fb520ad2a", + "age": 38, + "name": "Campbell Baxter", + "gender": "male", + "company": "DREAMIA", + "email": "campbellbaxter@dreamia.com", + "phone": "+1 (858) 524-3012" + }, + { + "_id": "55d2fc86460508fbed08e924", + "age": 23, + "name": "Lindsay Sharp", + "gender": "male", + "company": "SYBIXTEX", + "email": "lindsaysharp@sybixtex.com", + "phone": "+1 (974) 573-3073" + }, + { + "_id": "55d2fc860bf5295ce5679523", + "age": 35, + "name": "Kathrine Browning", + "gender": "female", + "company": "LUNCHPAD", + "email": "kathrinebrowning@lunchpad.com", + "phone": "+1 (922) 458-2466" + }, + { + "_id": "55d2fc86d2311e0208cd17a2", + "age": 35, + "name": "Alice Faulkner", + "gender": "female", + "company": "PLEXIA", + "email": "alicefaulkner@plexia.com", + "phone": "+1 (939) 419-3621" + }, + { + "_id": "55d2fc86e85116cc8d6c0d70", + "age": 32, + "name": "Ford Mclean", + "gender": "male", + "company": "QUADEEBO", + "email": "fordmclean@quadeebo.com", + "phone": "+1 (974) 521-2540" + }, + { + "_id": "55d2fc86f60393147a3a8a07", + "age": 29, + "name": "Ollie Cannon", + "gender": "female", + "company": "RAMJOB", + "email": "olliecannon@ramjob.com", + "phone": "+1 (961) 497-3201" + }, + { + "_id": "55d2fc8661d02152a5ba425a", + "age": 36, + "name": "Calderon Vaughan", + "gender": "male", + "company": "DIGIQUE", + "email": "calderonvaughan@digique.com", + "phone": "+1 (912) 553-3902" + }, + { + "_id": "55d2fc86a55890c6ce345bf0", + "age": 28, + "name": "Warren Henry", + "gender": "male", + "company": "ZERBINA", + "email": "warrenhenry@zerbina.com", + "phone": "+1 (850) 464-3656" + }, + { + "_id": "55d2fc86ccead2741ea21e0e", + "age": 21, + "name": "Liliana York", + "gender": "female", + "company": "ANIXANG", + "email": "lilianayork@anixang.com", + "phone": "+1 (956) 403-2096" + }, + { + "_id": "55d2fc86d72dd184f6884371", + "age": 33, + "name": "Lora Alvarez", + "gender": "female", + "company": "BLURRYBUS", + "email": "loraalvarez@blurrybus.com", + "phone": "+1 (917) 457-2866" + }, + { + "_id": "55d2fc86fda0c180ccc9598a", + "age": 22, + "name": "Luna Ellis", + "gender": "male", + "company": "SLUMBERIA", + "email": "lunaellis@slumberia.com", + "phone": "+1 (878) 589-3511" + }, + { + "_id": "55d2fc860dd81b364fc1c2a9", + "age": 30, + "name": "Phoebe Chang", + "gender": "female", + "company": "OPTICON", + "email": "phoebechang@opticon.com", + "phone": "+1 (962) 559-3475" + }, + { + "_id": "55d2fc8657954cc73c166579", + "age": 40, + "name": "Anna Crane", + "gender": "female", + "company": "AQUAFIRE", + "email": "annacrane@aquafire.com", + "phone": "+1 (989) 567-3649" + }, + { + "_id": "55d2fc86d996f0f466a006c8", + "age": 39, + "name": "Matthews French", + "gender": "male", + "company": "CINESANCT", + "email": "matthewsfrench@cinesanct.com", + "phone": "+1 (896) 518-2965" + }, + { + "_id": "55d2fc8601aad1428aa65531", + "age": 34, + "name": "Hutchinson Ellison", + "gender": "male", + "company": "MOREGANIC", + "email": "hutchinsonellison@moreganic.com", + "phone": "+1 (860) 563-2707" + }, + { + "_id": "55d2fc86ec14a6c798e22d72", + "age": 21, + "name": "Gwen Russell", + "gender": "female", + "company": "COMVOY", + "email": "gwenrussell@comvoy.com", + "phone": "+1 (873) 468-2314" + }, + { + "_id": "55d2fc865123af00125fd9bd", + "age": 22, + "name": "Natalie Stuart", + "gender": "female", + "company": "MOMENTIA", + "email": "nataliestuart@momentia.com", + "phone": "+1 (908) 573-2177" + }, + { + "_id": "55d2fc8688af31f1e9ff0d20", + "age": 28, + "name": "Brianna Meyer", + "gender": "female", + "company": "VIASIA", + "email": "briannameyer@viasia.com", + "phone": "+1 (860) 475-3139" + }, + { + "_id": "55d2fc8687f0ff5daa16cbbd", + "age": 28, + "name": "Trisha Castillo", + "gender": "female", + "company": "CYCLONICA", + "email": "trishacastillo@cyclonica.com", + "phone": "+1 (869) 564-2957" + }, + { + "_id": "55d2fc863b1c51f11ce4e921", + "age": 36, + "name": "Powers Weeks", + "gender": "male", + "company": "BIZMATIC", + "email": "powersweeks@bizmatic.com", + "phone": "+1 (981) 464-3668" + }, + { + "_id": "55d2fc86d8f5d2bef6f84bba", + "age": 23, + "name": "Young Cabrera", + "gender": "female", + "company": "CINASTER", + "email": "youngcabrera@cinaster.com", + "phone": "+1 (897) 528-3924" + }, + { + "_id": "55d2fc86861238b57e2932fd", + "age": 27, + "name": "Maxine Rodgers", + "gender": "female", + "company": "CHORIZON", + "email": "maxinerodgers@chorizon.com", + "phone": "+1 (996) 449-2805" + }, + { + "_id": "55d2fc86fe5b2f6823cc295f", + "age": 26, + "name": "Davis Norris", + "gender": "male", + "company": "CORIANDER", + "email": "davisnorris@coriander.com", + "phone": "+1 (947) 512-2093" + }, + { + "_id": "55d2fc86862e7d0bba1ab524", + "age": 25, + "name": "Ericka Conner", + "gender": "female", + "company": "STRALOY", + "email": "erickaconner@straloy.com", + "phone": "+1 (922) 565-2956" + }, + { + "_id": "55d2fc8625bf91e39382ab4c", + "age": 21, + "name": "Payne Joyner", + "gender": "male", + "company": "OMNIGOG", + "email": "paynejoyner@omnigog.com", + "phone": "+1 (998) 521-3917" + }, + { + "_id": "55d2fc866835ee51ccea79bb", + "age": 24, + "name": "Fletcher Payne", + "gender": "male", + "company": "AMTAP", + "email": "fletcherpayne@amtap.com", + "phone": "+1 (991) 517-3798" + }, + { + "_id": "55d2fc863df3424f70db684c", + "age": 38, + "name": "Mosley Cobb", + "gender": "male", + "company": "HONOTRON", + "email": "mosleycobb@honotron.com", + "phone": "+1 (873) 593-2248" + }, + { + "_id": "55d2fc867c4ad9b233d15983", + "age": 24, + "name": "Webster Sandoval", + "gender": "male", + "company": "HOMELUX", + "email": "webstersandoval@homelux.com", + "phone": "+1 (967) 431-2940" + }, + { + "_id": "55d2fc8613bf2fe49b1a1f8c", + "age": 36, + "name": "Colon Mcgee", + "gender": "male", + "company": "ZAPPIX", + "email": "colonmcgee@zappix.com", + "phone": "+1 (806) 444-2451" + }, + { + "_id": "55d2fc8681a8ccebe8aacd93", + "age": 32, + "name": "Monique Logan", + "gender": "female", + "company": "CALLFLEX", + "email": "moniquelogan@callflex.com", + "phone": "+1 (957) 577-3780" + }, + { + "_id": "55d2fc868756d302f29fddb2", + "age": 38, + "name": "Stewart Ball", + "gender": "male", + "company": "NETPLODE", + "email": "stewartball@netplode.com", + "phone": "+1 (966) 435-2206" + }, + { + "_id": "55d2fc86457f4d474388d2c8", + "age": 23, + "name": "Montgomery Carter", + "gender": "male", + "company": "OLUCORE", + "email": "montgomerycarter@olucore.com", + "phone": "+1 (894) 556-2662" + }, + { + "_id": "55d2fc866da04e0d15b12b36", + "age": 28, + "name": "Brenda Mccoy", + "gender": "female", + "company": "AQUAZURE", + "email": "brendamccoy@aquazure.com", + "phone": "+1 (837) 483-3741" + }, + { + "_id": "55d2fc86e2a61a730a8d5c8f", + "age": 34, + "name": "Eddie Buchanan", + "gender": "female", + "company": "COGNICODE", + "email": "eddiebuchanan@cognicode.com", + "phone": "+1 (924) 479-3753" + }, + { + "_id": "55d2fc8616e7989042f61488", + "age": 23, + "name": "Eva Mendoza", + "gender": "female", + "company": "SOLGAN", + "email": "evamendoza@solgan.com", + "phone": "+1 (899) 522-3051" + }, + { + "_id": "55d2fc86152fd9e4c5471fd7", + "age": 21, + "name": "Dawson Medina", + "gender": "male", + "company": "AQUASSEUR", + "email": "dawsonmedina@aquasseur.com", + "phone": "+1 (877) 580-2295" + }, + { + "_id": "55d2fc86a3633b3a799a7811", + "age": 34, + "name": "Terrie Hobbs", + "gender": "female", + "company": "VORATAK", + "email": "terriehobbs@voratak.com", + "phone": "+1 (938) 511-2077" + }, + { + "_id": "55d2fc8665a05f05f07bb790", + "age": 37, + "name": "Iris Bishop", + "gender": "female", + "company": "INSURESYS", + "email": "irisbishop@insuresys.com", + "phone": "+1 (819) 415-3840" + }, + { + "_id": "55d2fc8799e7556a033b93f6", + "age": 29, + "name": "Estelle Grant", + "gender": "female", + "company": "ZOGAK", + "email": "estellegrant@zogak.com", + "phone": "+1 (854) 437-2898" + }, + { + "_id": "55d2fc87745c675e697af04c", + "age": 30, + "name": "Dianna Gonzalez", + "gender": "female", + "company": "PIVITOL", + "email": "diannagonzalez@pivitol.com", + "phone": "+1 (816) 545-3520" + } +] diff --git a/gzhttp/transport.go b/gzhttp/transport.go new file mode 100644 index 0000000000..85abe9d0bd --- /dev/null +++ b/gzhttp/transport.go @@ -0,0 +1,116 @@ +// Copyright (c) 2021 Klaus Post. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gzhttp + +import ( + "io" + "net/http" + "sync" + + "github.com/klauspost/compress/gzip" +) + +// Transport will wrap a transport with a custom gzip handler +// that will request gzip and automatically decompress it. +// Using this is significantly faster than using the default transport. +func Transport(parent http.RoundTripper) http.RoundTripper { + return gzRoundtripper{parent: parent} +} + +type gzRoundtripper struct { + parent http.RoundTripper +} + +func (g gzRoundtripper) RoundTrip(req *http.Request) (*http.Response, error) { + var requestedGzip bool + if req.Header.Get("Accept-Encoding") == "" && + req.Header.Get("Range") == "" && + req.Method != "HEAD" { + // Request gzip only, not deflate. Deflate is ambiguous and + // not as universally supported anyway. + // See: https://zlib.net/zlib_faq.html#faq39 + // + // Note that we don't request this for HEAD requests, + // due to a bug in nginx: + // https://trac.nginx.org/nginx/ticket/358 + // https://golang.org/issue/5522 + // + // We don't request gzip if the request is for a range, since + // auto-decoding a portion of a gzipped document will just fail + // anyway. See https://golang.org/issue/8923 + requestedGzip = true + req.Header.Set("Accept-Encoding", "gzip") + } + resp, err := g.parent.RoundTrip(req) + if err != nil || !requestedGzip { + return resp, err + } + if asciiEqualFold(resp.Header.Get("Content-Encoding"), "gzip") { + resp.Body = &gzipReader{body: resp.Body} + resp.Header.Del("Content-Encoding") + resp.Header.Del("Content-Length") + resp.ContentLength = -1 + resp.Uncompressed = true + } + return resp, nil +} + +var gzReaderPool sync.Pool + +// gzipReader wraps a response body so it can lazily +// call gzip.NewReader on the first call to Read +type gzipReader struct { + body io.ReadCloser // underlying HTTP/1 response body framing + zr *gzip.Reader // lazily-initialized gzip reader + zerr error // any error from gzip.NewReader; sticky +} + +func (gz *gzipReader) Read(p []byte) (n int, err error) { + if gz.zr == nil { + if gz.zerr == nil { + zr, ok := gzReaderPool.Get().(*gzip.Reader) + if ok { + gz.zr, gz.zerr = zr, zr.Reset(gz.body) + } else { + gz.zr, gz.zerr = gzip.NewReader(gz.body) + } + } + if gz.zerr != nil { + return 0, gz.zerr + } + } + + return gz.zr.Read(p) +} + +func (gz *gzipReader) Close() error { + if gz.zr != nil { + gzReaderPool.Put(gz.zr) + gz.zr = nil + } + return gz.body.Close() +} + +// asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t +// are equal, ASCII-case-insensitively. +func asciiEqualFold(s, t string) bool { + if len(s) != len(t) { + return false + } + for i := 0; i < len(s); i++ { + if lower(s[i]) != lower(t[i]) { + return false + } + } + return true +} + +// lower returns the ASCII lowercase version of b. +func lower(b byte) byte { + if 'A' <= b && b <= 'Z' { + return b + ('a' - 'A') + } + return b +} diff --git a/gzhttp/transport_test.go b/gzhttp/transport_test.go new file mode 100644 index 0000000000..e558145d69 --- /dev/null +++ b/gzhttp/transport_test.go @@ -0,0 +1,174 @@ +// Copyright (c) 2021 Klaus Post. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gzhttp + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/klauspost/compress/gzip" +) + +func TestTransport(t *testing.T) { + bin, err := ioutil.ReadFile("testdata/benchmark.json") + if err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(newTestHandler(string(bin))) + + c := http.Client{Transport: Transport(http.DefaultTransport)} + resp, err := c.Get(server.URL) + if err != nil { + t.Fatal(err) + } + got, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, bin) { + t.Errorf("data mismatch") + } +} + +func TestTransportInvalid(t *testing.T) { + bin, err := ioutil.ReadFile("testdata/benchmark.json") + if err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(newTestHandler(string(bin))) + + c := http.Client{Transport: Transport(http.DefaultTransport)} + // Serves json as gzippped... + resp, err := c.Get(server.URL + "/gzipped") + if err != nil { + t.Fatal(err) + } + _, err = ioutil.ReadAll(resp.Body) + if err == nil { + t.Fatal("expected error, got nil") + } +} + +func TestDefaultTransport(t *testing.T) { + bin, err := ioutil.ReadFile("testdata/benchmark.json") + if err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(newTestHandler(string(bin))) + + // Not wrapped... + c := http.Client{Transport: http.DefaultTransport} + resp, err := c.Get(server.URL) + if err != nil { + t.Fatal(err) + } + got, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, bin) { + t.Errorf("data mismatch") + } +} + +func BenchmarkTransport(b *testing.B) { + bin, err := ioutil.ReadFile("testdata/benchmark.json") + if err != nil { + b.Fatal(err) + } + sz := len(bin) + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + zw.Write(bin) + zw.Close() + bin = buf.Bytes() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Encoding", "gzip") + w.Write(bin) + })) + b.Run("gzhttp", func(b *testing.B) { + c := http.Client{Transport: Transport(http.DefaultTransport)} + + b.SetBytes(int64(sz)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp, err := c.Get(server.URL + "/gzipped") + if err != nil { + b.Fatal(err) + } + _, err = io.Copy(ioutil.Discard, resp.Body) + if err != nil { + b.Fatal(err) + } + resp.Body.Close() + } + }) + b.Run("stdlib", func(b *testing.B) { + c := http.Client{Transport: http.DefaultTransport} + b.SetBytes(int64(sz)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp, err := c.Get(server.URL + "/gzipped") + if err != nil { + b.Fatal(err) + } + _, err = io.Copy(ioutil.Discard, resp.Body) + if err != nil { + b.Fatal(err) + } + resp.Body.Close() + } + }) + b.Run("gzhttp-par", func(b *testing.B) { + c := http.Client{ + Transport: Transport(http.DefaultTransport), + } + + b.SetBytes(int64(sz)) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + resp, err := c.Get(server.URL + "/gzipped") + if err != nil { + b.Fatal(err) + } + _, err = io.Copy(ioutil.Discard, resp.Body) + if err != nil { + b.Fatal(err) + } + resp.Body.Close() + } + }) + }) + b.Run("stdlib-par", func(b *testing.B) { + c := http.Client{Transport: http.DefaultTransport} + b.SetBytes(int64(sz)) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + resp, err := c.Get(server.URL + "/gzipped") + if err != nil { + b.Fatal(err) + } + _, err = io.Copy(ioutil.Discard, resp.Body) + if err != nil { + b.Fatal(err) + } + resp.Body.Close() + } + }) + }) +} From ab5d6942181eb3c97eb0c995fb14f1c7caedf348 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Tue, 1 Jun 2021 15:50:45 +0200 Subject: [PATCH 11/14] Make sure to reuse connections. --- gzhttp/README.md | 5 ++--- gzhttp/transport_test.go | 13 +++++++++++-- gzip/gunzip.go | 9 ++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/gzhttp/README.md b/gzhttp/README.md index 7e39449c91..37c3707451 100644 --- a/gzhttp/README.md +++ b/gzhttp/README.md @@ -63,12 +63,11 @@ BenchmarkTransport/gzhttp-32 1995 609791 ns/op 214.14 MB/s BenchmarkTransport/stdlib-32 1567 772161 ns/op 169.11 MB/s 53950 B/op 99 allocs/op Multi Core: -BenchmarkTransport/gzhttp-par-32 8428 199682 ns/op 653.95 MB/s 42332 B/op 120 allocs/op -BenchmarkTransport/stdlib-par-32 3946 311886 ns/op 418.69 MB/s 71972 B/op 137 allocs/op +BenchmarkTransport/gzhttp-par-32 29113 36802 ns/op 3548.27 MB/s 11061 B/op 73 allocs/op +BenchmarkTransport/stdlib-par-32 16114 66442 ns/op 1965.38 MB/s 54971 B/op 99 allocs/op ``` This includes both serving the http request, parsing requests and decompressing. -For this payload size decompression is actually only about 40% of CPU load on the multi-core benchmark. ### Server diff --git a/gzhttp/transport_test.go b/gzhttp/transport_test.go index e558145d69..d953b98abd 100644 --- a/gzhttp/transport_test.go +++ b/gzhttp/transport_test.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "runtime" "testing" "github.com/klauspost/compress/gzip" @@ -92,7 +93,9 @@ func BenchmarkTransport(b *testing.B) { zw.Close() bin = buf.Bytes() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Body.Close() w.Header().Set("Content-Encoding", "gzip") + w.WriteHeader(http.StatusOK) w.Write(bin) })) b.Run("gzhttp", func(b *testing.B) { @@ -132,7 +135,10 @@ func BenchmarkTransport(b *testing.B) { }) b.Run("gzhttp-par", func(b *testing.B) { c := http.Client{ - Transport: Transport(http.DefaultTransport), + Transport: Transport(&http.Transport{ + MaxConnsPerHost: runtime.GOMAXPROCS(0), + MaxIdleConnsPerHost: runtime.GOMAXPROCS(0), + }), } b.SetBytes(int64(sz)) @@ -153,7 +159,10 @@ func BenchmarkTransport(b *testing.B) { }) }) b.Run("stdlib-par", func(b *testing.B) { - c := http.Client{Transport: http.DefaultTransport} + c := http.Client{Transport: &http.Transport{ + MaxConnsPerHost: runtime.GOMAXPROCS(0), + MaxIdleConnsPerHost: runtime.GOMAXPROCS(0), + }} b.SetBytes(int64(sz)) b.ReportAllocs() b.ResetTimer() diff --git a/gzip/gunzip.go b/gzip/gunzip.go index 568b5d4fb8..21e768b360 100644 --- a/gzip/gunzip.go +++ b/gzip/gunzip.go @@ -75,6 +75,7 @@ type Header struct { type Reader struct { Header // valid after NewReader or Reader.Reset r flate.Reader + br *bufio.Reader decompressor io.ReadCloser digest uint32 // CRC-32, IEEE polynomial (section 8) size uint32 // Uncompressed size (section 2.3.1) @@ -109,7 +110,13 @@ func (z *Reader) Reset(r io.Reader) error { if rr, ok := r.(flate.Reader); ok { z.r = rr } else { - z.r = bufio.NewReader(r) + // Reuse if we can. + if z.br != nil { + z.br.Reset(r) + } else { + z.br = bufio.NewReader(r) + } + z.r = z.br } z.Header, z.err = z.readHeader() return z.err From 7c3644ac6f49a1f7c07ff945355ade53dd924e7b Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Wed, 2 Jun 2021 09:56:27 +0200 Subject: [PATCH 12/14] Strip Accept-Ranges on compressed content Fixes https://github.com/nytimes/gziphandler/issues/83 Adds `KeepAcceptRanges()` if for whatever reason you would want to keep them. --- gzhttp/gzip.go | 38 ++++++++++++++++++++------ gzhttp/gzip_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/gzhttp/gzip.go b/gzhttp/gzip.go index 0142a6f531..9fabcdc023 100644 --- a/gzhttp/gzip.go +++ b/gzhttp/gzip.go @@ -20,6 +20,8 @@ const ( vary = "Vary" acceptEncoding = "Accept-Encoding" contentEncoding = "Content-Encoding" + contentRange = "Content-Range" + acceptRanges = "Accept-Ranges" contentType = "Content-Type" contentLength = "Content-Length" ) @@ -51,9 +53,10 @@ type GzipResponseWriter struct { code int // Saves the WriteHeader value. - minSize int // Specifies the minimum response size to gzip. If the response length is bigger than this value, it is compressed. - buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. - ignore bool // If true, then we immediately passthru writes to the underlying ResponseWriter. + minSize int // Specifies the minimum response size to gzip. If the response length is bigger than this value, it is compressed. + buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. + ignore bool // If true, then we immediately passthru writes to the underlying ResponseWriter. + keepAcceptRanges bool // Keep "Accept-Ranges" header. contentTypeFilter func(ct string) bool // Only compress if the response is one of these content-types. All are accepted if empty. } @@ -86,13 +89,16 @@ func (w *GzipResponseWriter) Write(b []byte) (int, error) { cl, _ = strconv.Atoi(w.Header().Get(contentLength)) ct = w.Header().Get(contentType) ce = w.Header().Get(contentEncoding) + cr = w.Header().Get(contentRange) ) + // Only continue if they didn't already choose an encoding or a known unhandled content length or type. - if ce == "" && (cl == 0 || cl >= w.minSize) && (ct == "" || w.contentTypeFilter(ct)) { + if ce == "" && cr == "" && (cl == 0 || cl >= w.minSize) && (ct == "" || w.contentTypeFilter(ct)) { // If the current buffer is less than minSize and a Content-Length isn't set, then wait until we have more data. if len(w.buf) < w.minSize && cl == 0 { return len(b), nil } + // If the Content-Length is larger than minSize or the current buffer is larger than minSize, then continue. if cl >= w.minSize || len(w.buf) >= w.minSize { // If a Content-Type wasn't specified, infer it from the current buffer. @@ -132,6 +138,11 @@ func (w *GzipResponseWriter) startGzip() error { // See: https://github.com/golang/go/issues/14975. w.Header().Del(contentLength) + // Delete Accept-Ranges. + if !w.keepAcceptRanges { + w.Header().Del(acceptRanges) + } + // Write the header to gzip response. if w.code != 0 { w.ResponseWriter.WriteHeader(w.code) @@ -297,6 +308,7 @@ func NewWrapper(opts ...option) (func(http.Handler) http.Handler, error) { level: c.level, minSize: c.minSize, contentTypeFilter: c.contentTypes, + keepAcceptRanges: c.keepAcceptRanges, } defer gw.Close() @@ -345,10 +357,11 @@ func (pct parsedContentType) equals(mediaType string, params map[string]string) // Used for functional configuration. type config struct { - minSize int - level int - writer writer.GzipWriterFactory - contentTypes func(ct string) bool + minSize int + level int + writer writer.GzipWriterFactory + contentTypes func(ct string) bool + keepAcceptRanges bool } func (c *config) validate() error { @@ -457,6 +470,15 @@ func ExceptContentTypes(types []string) option { } } +// KeepAcceptRanges will keep Accept-Ranges header on gzipped responses. +// This will likely break ranged requests since that cannot be transparently +// handled by the filter. +func KeepAcceptRanges() option { + return func(c *config) { + c.keepAcceptRanges = true + } +} + // ContentTypeFilter allows adding a custom content type filter. // // The supplied function must return true/false to indicate if content diff --git a/gzhttp/gzip_test.go b/gzhttp/gzip_test.go index 10075c1242..fc6cd6840b 100644 --- a/gzhttp/gzip_test.go +++ b/gzhttp/gzip_test.go @@ -114,6 +114,72 @@ func TestGzipHandlerAlreadyCompressed(t *testing.T) { assertEqual(t, testBody, res.Body.String()) } +func TestGzipHandlerRangeReply(t *testing.T) { + handler := GzipHandler( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Range", "bytes 0-300/804") + w.WriteHeader(http.StatusOK) + w.Write([]byte(testBody)) + })) + req, _ := http.NewRequest("GET", "/gzipped", nil) + req.Header.Set("Accept-Encoding", "gzip") + + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + res := resp.Result() + assertEqual(t, 200, res.StatusCode) + assertEqual(t, "", res.Header.Get("Content-Encoding")) + assertEqual(t, testBody, resp.Body.String()) +} + +func TestGzipHandlerAcceptRange(t *testing.T) { + handler := GzipHandler( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Accept-Ranges", "bytes") + w.WriteHeader(http.StatusOK) + w.Write([]byte(testBody)) + })) + req, _ := http.NewRequest("GET", "/gzipped", nil) + req.Header.Set("Accept-Encoding", "gzip") + + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + res := resp.Result() + assertEqual(t, 200, res.StatusCode) + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + assertEqual(t, "", res.Header.Get("Accept-Ranges")) + zr, err := gzip.NewReader(resp.Body) + assertNil(t, err) + got, err := ioutil.ReadAll(zr) + assertNil(t, err) + assertEqual(t, testBody, string(got)) +} + +func TestGzipHandlerKeepAcceptRange(t *testing.T) { + wrapper, err := NewWrapper(KeepAcceptRanges()) + assertNil(t, err) + handler := wrapper( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Accept-Ranges", "bytes") + w.WriteHeader(http.StatusOK) + w.Write([]byte(testBody)) + })) + req, _ := http.NewRequest("GET", "/gzipped", nil) + req.Header.Set("Accept-Encoding", "gzip") + + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + res := resp.Result() + assertEqual(t, 200, res.StatusCode) + assertEqual(t, "gzip", res.Header.Get("Content-Encoding")) + assertEqual(t, "bytes", res.Header.Get("Accept-Ranges")) + zr, err := gzip.NewReader(resp.Body) + assertNil(t, err) + got, err := ioutil.ReadAll(zr) + assertNil(t, err) + assertEqual(t, testBody, string(got)) +} + func TestNewGzipLevelHandler(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) From 4f8b0149c7a8bab9c1141607a5a887dfc84f335a Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Wed, 2 Jun 2021 13:26:44 +0200 Subject: [PATCH 13/14] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 65353f1e1e..5e35024a69 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This package provides various compression algorithms. * [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) is a high performance replacement for Snappy. * Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib). * [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding. +* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped requests efficiently. * [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation. * [fuzz package](https://github.com/klauspost/compress-fuzz) for fuzz testing all compressors/decompressors here. From fd6bf8e8ea8dfad8570a26bd939eaa10dbcc7c95 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Wed, 2 Jun 2021 13:34:51 +0200 Subject: [PATCH 14/14] Update docs. --- gzhttp/writer/gzkp/gzkp.go | 2 ++ gzhttp/writer/gzstd/stdlib.go | 2 ++ gzhttp/writer/interface.go | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gzhttp/writer/gzkp/gzkp.go b/gzhttp/writer/gzkp/gzkp.go index ed01074253..053f01247c 100644 --- a/gzhttp/writer/gzkp/gzkp.go +++ b/gzhttp/writer/gzkp/gzkp.go @@ -1,3 +1,5 @@ +// package gzkp provides gzip compression through github.com/klauspost/compress/gzip. + package gzkp import ( diff --git a/gzhttp/writer/gzstd/stdlib.go b/gzhttp/writer/gzstd/stdlib.go index 0e7abd9117..b3bc1e5e0a 100644 --- a/gzhttp/writer/gzstd/stdlib.go +++ b/gzhttp/writer/gzstd/stdlib.go @@ -1,3 +1,5 @@ +// package gzstd provides gzip compression through the standard library. + package gzstd import ( diff --git a/gzhttp/writer/interface.go b/gzhttp/writer/interface.go index 04c739d4ef..20c5161297 100644 --- a/gzhttp/writer/interface.go +++ b/gzhttp/writer/interface.go @@ -4,12 +4,12 @@ import "io" // GzipWriter implements the functions needed for compressing content. type GzipWriter interface { + Write(p []byte) (int, error) Close() error Flush() error - Write(p []byte) (int, error) } -// GzipWriterFactory contains the information needed for the +// GzipWriterFactory contains the information needed for custom gzip implementations. type GzipWriterFactory struct { // Must return the minimum and maximum supported level. Levels func() (min, max int)