# HG changeset patch # User Maxim Dounin # Date 1260653043 -10800 # Node ID a75d4ad9c5d23455dc324a9b7e7836b7007f32f2 Gunzip filter module. diff --git a/LICENSE b/LICENSE new file mode 100644 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2002-2009 Igor Sysoev + * Copyright (C) 2009 Maxim Dounin + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ diff --git a/README b/README new file mode 100644 --- /dev/null +++ b/README @@ -0,0 +1,31 @@ +Gunzip module for nginx. + +This module allows ungzipping responses returned with Content-Encoding: gzip +for clients that doesn't support it. It may be usefull if you prefer to store +data compressed (to save space or disk/network IO) but do not want to penalize +clients without gzip support. + +Configuration directives: + + gunzip (on|off) + + Context: http, server, location + Default: off + + Switches gunzip. + + gunzip_buffers + + Context: http, server, location + Default: 32 4k/16 8k + + Specifies number and size of buffers available for decompression. + +Usage: + + location /storage/ { + gunzip on; + } + +To compile nginx with gunzip module, use "--add-module " option to nginx +configure. diff --git a/config b/config new file mode 100644 --- /dev/null +++ b/config @@ -0,0 +1,10 @@ +# (C) Maxim Dounin +# Configuration for ngx_http_gunzip_filter_module. + +ngx_addon_name="ngx_http_gunzip_filter_module" + +HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES \ + ngx_http_gunzip_filter_module" + +NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ + $ngx_addon_dir/ngx_http_gunzip_filter_module.c" diff --git a/ngx_http_gunzip_filter_module.c b/ngx_http_gunzip_filter_module.c new file mode 100644 --- /dev/null +++ b/ngx_http_gunzip_filter_module.c @@ -0,0 +1,689 @@ + +/* + * Copyright (C) Igor Sysoev + * Copyright (C) Maxim Dounin + */ + + +#include +#include +#include +#include + +#include + + +typedef struct { + ngx_flag_t enable; + ngx_bufs_t bufs; +} ngx_http_gunzip_conf_t; + + +typedef struct { + ngx_chain_t *in; + ngx_chain_t *free; + ngx_chain_t *busy; + ngx_chain_t *out; + ngx_chain_t **last_out; + + ngx_buf_t *in_buf; + ngx_buf_t *out_buf; + ngx_int_t bufs; + + unsigned started:1; + unsigned flush:4; + unsigned redo:1; + unsigned done:1; + unsigned nomem:1; + + z_stream zstream; + ngx_http_request_t *request; +} ngx_http_gunzip_ctx_t; + + +static ngx_int_t ngx_http_gunzip_filter_inflate_start(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx); +static ngx_int_t ngx_http_gunzip_filter_add_data(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx); +static ngx_int_t ngx_http_gunzip_filter_get_buf(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx); +static ngx_int_t ngx_http_gunzip_filter_inflate(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx); +static ngx_int_t ngx_http_gunzip_filter_inflate_end(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx); + +static void *ngx_http_gunzip_filter_alloc(void *opaque, u_int items, + u_int size); +static void ngx_http_gunzip_filter_free(void *opaque, void *address); + +static ngx_int_t ngx_http_gunzip_filter_init(ngx_conf_t *cf); +static void *ngx_http_gunzip_create_conf(ngx_conf_t *cf); +static char *ngx_http_gunzip_merge_conf(ngx_conf_t *cf, + void *parent, void *child); + + +static ngx_command_t ngx_http_gunzip_filter_commands[] = { + + { ngx_string("gunzip"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_gunzip_conf_t, enable), + NULL }, + + { ngx_string("gunzip_buffers"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2, + ngx_conf_set_bufs_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_gunzip_conf_t, bufs), + NULL }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_http_gunzip_filter_module_ctx = { + NULL, /* preconfiguration */ + ngx_http_gunzip_filter_init, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_http_gunzip_create_conf, /* create location configuration */ + ngx_http_gunzip_merge_conf /* merge location configuration */ +}; + + +ngx_module_t ngx_http_gunzip_filter_module = { + NGX_MODULE_V1, + &ngx_http_gunzip_filter_module_ctx, /* module context */ + ngx_http_gunzip_filter_commands, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static ngx_http_output_header_filter_pt ngx_http_next_header_filter; +static ngx_http_output_body_filter_pt ngx_http_next_body_filter; + + +static ngx_int_t +ngx_http_gunzip_header_filter(ngx_http_request_t *r) +{ + ngx_http_gunzip_ctx_t *ctx; + ngx_http_gunzip_conf_t *conf; + + conf = ngx_http_get_module_loc_conf(r, ngx_http_gunzip_filter_module); + + /* TODO support multiple content-codings */ + + if (!conf->enable + || r->headers_out.content_encoding == NULL + || r->headers_out.content_encoding->value.len != 4 + || ngx_strncasecmp(r->headers_out.content_encoding->value.data, + (u_char *) "gzip", 4) != 0) + { + return ngx_http_next_header_filter(r); + } + +#if (nginx_version >= 8025) + + r->gzip_vary = 1; + + if (!r->gzip_tested) { + if (ngx_http_gzip_ok(r) == NGX_OK) { + return ngx_http_next_header_filter(r); + } + + } else if (!r->gzip_ok) { + return ngx_http_next_header_filter(r); + } + +#else + + if (ngx_http_gzip_ok(r) == NGX_OK) { + return ngx_http_next_header_filter(r); + } + +#endif + + ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_gunzip_ctx_t)); + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_http_set_ctx(r, ctx, ngx_http_gunzip_filter_module); + + ctx->request = r; + + r->filter_need_in_memory = 1; + + r->headers_out.content_encoding->hash = 0; + r->headers_out.content_encoding = NULL; + + ngx_http_clear_content_length(r); + ngx_http_clear_accept_ranges(r); + + return ngx_http_next_header_filter(r); +} + + +static ngx_int_t +ngx_http_gunzip_body_filter(ngx_http_request_t *r, ngx_chain_t *in) +{ + int rc; + ngx_chain_t *cl; + ngx_http_gunzip_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_http_gunzip_filter_module); + + if (ctx == NULL || ctx->done) { + return ngx_http_next_body_filter(r, in); + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http gunzip filter"); + + if (!ctx->started) { + if (ngx_http_gunzip_filter_inflate_start(r, ctx) != NGX_OK) { + goto failed; + } + } + + if (in) { + if (ngx_chain_add_copy(r->pool, &ctx->in, in) != NGX_OK) { + goto failed; + } + } + + if (ctx->nomem) { + + /* flush busy buffers */ + + if (ngx_http_next_body_filter(r, NULL) == NGX_ERROR) { + goto failed; + } + + cl = NULL; + + ngx_chain_update_chains(&ctx->free, &ctx->busy, &cl, + (ngx_buf_tag_t) &ngx_http_gunzip_filter_module); + ctx->nomem = 0; + } + + for ( ;; ) { + + /* cycle while we can write to a client */ + + for ( ;; ) { + + /* cycle while there is data to feed zlib and ... */ + + rc = ngx_http_gunzip_filter_add_data(r, ctx); + + if (rc == NGX_DECLINED) { + break; + } + + if (rc == NGX_AGAIN) { + continue; + } + + + /* ... there are buffers to write zlib output */ + + rc = ngx_http_gunzip_filter_get_buf(r, ctx); + + if (rc == NGX_DECLINED) { + break; + } + + if (rc == NGX_ERROR) { + goto failed; + } + + rc = ngx_http_gunzip_filter_inflate(r, ctx); + + if (rc == NGX_OK) { + break; + } + + if (rc == NGX_ERROR) { + goto failed; + } + + /* rc == NGX_AGAIN */ + } + + if (ctx->out == NULL) { + return ctx->busy ? NGX_AGAIN : NGX_OK; + } + + rc = ngx_http_next_body_filter(r, ctx->out); + + if (rc == NGX_ERROR) { + goto failed; + } + + ngx_chain_update_chains(&ctx->free, &ctx->busy, &ctx->out, + (ngx_buf_tag_t) &ngx_http_gunzip_filter_module); + ctx->last_out = &ctx->out; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "gunzip out: %p", ctx->out); + + ctx->nomem = 0; + + if (ctx->done) { + return rc; + } + } + + /* unreachable */ + +failed: + + ctx->done = 1; + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_http_gunzip_filter_inflate_start(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx) +{ + int rc; + + ctx->zstream.next_in = Z_NULL; + ctx->zstream.avail_in = 0; + + ctx->zstream.zalloc = ngx_http_gunzip_filter_alloc; + ctx->zstream.zfree = ngx_http_gunzip_filter_free; + ctx->zstream.opaque = ctx; + + /* windowBits +16 to decode gzip, zlib 1.2.0.4+ */ + rc = inflateInit2(&ctx->zstream, MAX_WBITS + 16); + + if (rc != Z_OK) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "inflateInit2() failed: %d", rc); + return NGX_ERROR; + } + + ctx->started = 1; + + ctx->last_out = &ctx->out; + ctx->flush = Z_NO_FLUSH; + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_gunzip_filter_add_data(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx) +{ + if (ctx->zstream.avail_in || ctx->flush != Z_NO_FLUSH || ctx->redo) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "gunzip in: %p", ctx->in); + + if (ctx->in == NULL) { + return NGX_DECLINED; + } + + ctx->in_buf = ctx->in->buf; + ctx->in = ctx->in->next; + + ctx->zstream.next_in = ctx->in_buf->pos; + ctx->zstream.avail_in = ctx->in_buf->last - ctx->in_buf->pos; + + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "gunzip in_buf:%p ni:%p ai:%ud", + ctx->in_buf, + ctx->zstream.next_in, ctx->zstream.avail_in); + + if (ctx->in_buf->last_buf || ctx->in_buf->last_in_chain) { + ctx->flush = Z_FINISH; + + } else if (ctx->in_buf->flush) { + ctx->flush = Z_SYNC_FLUSH; + + } else if (ctx->zstream.avail_in == 0) { + /* ctx->flush == Z_NO_FLUSH */ + return NGX_AGAIN; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_gunzip_filter_get_buf(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx) +{ + ngx_http_gunzip_conf_t *conf; + + if (ctx->zstream.avail_out) { + return NGX_OK; + } + + conf = ngx_http_get_module_loc_conf(r, ngx_http_gunzip_filter_module); + + if (ctx->free) { + ctx->out_buf = ctx->free->buf; + ctx->free = ctx->free->next; + + ctx->out_buf->flush = 0; + + } else if (ctx->bufs < conf->bufs.num) { + + ctx->out_buf = ngx_create_temp_buf(r->pool, conf->bufs.size); + if (ctx->out_buf == NULL) { + return NGX_ERROR; + } + + ctx->out_buf->tag = (ngx_buf_tag_t) &ngx_http_gunzip_filter_module; + ctx->out_buf->recycled = 1; + ctx->bufs++; + + } else { + ctx->nomem = 1; + return NGX_DECLINED; + } + + ctx->zstream.next_out = ctx->out_buf->pos; + ctx->zstream.avail_out = conf->bufs.size; + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_gunzip_filter_inflate(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx) +{ + int rc; + ngx_buf_t *b; + ngx_chain_t *cl; + + ngx_log_debug6(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "inflate in: ni:%p no:%p ai:%ud ao:%ud fl:%d redo:%d", + ctx->zstream.next_in, ctx->zstream.next_out, + ctx->zstream.avail_in, ctx->zstream.avail_out, + ctx->flush, ctx->redo); + + rc = inflate(&ctx->zstream, ctx->flush); + + if (rc != Z_OK && rc != Z_STREAM_END && rc != Z_BUF_ERROR) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "inflate() failed: %d, %d", ctx->flush, rc); + return NGX_ERROR; + } + + ngx_log_debug5(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "inflate out: ni:%p no:%p ai:%ud ao:%ud rc:%d", + ctx->zstream.next_in, ctx->zstream.next_out, + ctx->zstream.avail_in, ctx->zstream.avail_out, + rc); + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "gunzip in_buf:%p pos:%p", + ctx->in_buf, ctx->in_buf->pos); + + if (ctx->zstream.next_in) { + ctx->in_buf->pos = ctx->zstream.next_in; + + if (ctx->zstream.avail_in == 0) { + ctx->zstream.next_in = NULL; + } + } + + ctx->out_buf->last = ctx->zstream.next_out; + + if (ctx->zstream.avail_out == 0) { + + /* zlib wants to output some more data */ + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return NGX_ERROR; + } + + cl->buf = ctx->out_buf; + cl->next = NULL; + *ctx->last_out = cl; + ctx->last_out = &cl->next; + + ctx->redo = 1; + + return NGX_AGAIN; + } + + ctx->redo = 0; + + if (ctx->flush == Z_SYNC_FLUSH) { + + ctx->flush = Z_NO_FLUSH; + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return NGX_ERROR; + } + + b = ctx->out_buf; + + if (ngx_buf_size(b) == 0) { + + b = ngx_calloc_buf(ctx->request->pool); + if (b == NULL) { + return NGX_ERROR; + } + + } else { + ctx->zstream.avail_out = 0; + } + + b->flush = 1; + + cl->buf = b; + cl->next = NULL; + *ctx->last_out = cl; + ctx->last_out = &cl->next; + + return NGX_OK; + } + + if (rc == Z_STREAM_END && ctx->flush == Z_FINISH + && ctx->zstream.avail_in == 0) + { + + if (ngx_http_gunzip_filter_inflate_end(r, ctx) != NGX_OK) { + return NGX_ERROR; + } + + return NGX_OK; + } + + if (rc == Z_STREAM_END && ctx->zstream.avail_in > 0) { + + rc = inflateReset(&ctx->zstream); + + if (rc != Z_OK) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "inflateReset() failed: %d", rc); + return NGX_ERROR; + } + + ctx->redo = 1; + + return NGX_AGAIN; + } + + if (ctx->in == NULL) { + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return NGX_ERROR; + } + + b = ctx->out_buf; + + if (ngx_buf_size(b) == 0) { + + b = ngx_calloc_buf(ctx->request->pool); + if (b == NULL) { + return NGX_ERROR; + } + + } else { + ctx->zstream.avail_out = 0; + } + + cl->buf = b; + cl->next = NULL; + *ctx->last_out = cl; + ctx->last_out = &cl->next; + + return NGX_OK; + } + + return NGX_AGAIN; +} + + +static ngx_int_t +ngx_http_gunzip_filter_inflate_end(ngx_http_request_t *r, + ngx_http_gunzip_ctx_t *ctx) +{ + int rc; + ngx_buf_t *b; + ngx_chain_t *cl; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "gunzip inflate end"); + + rc = inflateEnd(&ctx->zstream); + + if (rc != Z_OK) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "inflateEnd() failed: %d", rc); + return NGX_ERROR; + } + + b = ctx->out_buf; + + if (ngx_buf_size(b) == 0) { + + b = ngx_calloc_buf(ctx->request->pool); + if (b == NULL) { + return NGX_ERROR; + } + } + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return NGX_ERROR; + } + + cl->buf = b; + cl->next = NULL; + *ctx->last_out = cl; + ctx->last_out = &cl->next; + + b->last_buf = (r == r->main) ? 1 : 0; + b->last_in_chain = 1; + b->sync = 1; + + ctx->done = 1; + + return NGX_OK; +} + + +static void * +ngx_http_gunzip_filter_alloc(void *opaque, u_int items, u_int size) +{ + ngx_http_gunzip_ctx_t *ctx = opaque; + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, ctx->request->connection->log, 0, + "gunzip alloc: n:%ud s:%ud", + items, size); + + return ngx_palloc(ctx->request->pool, items * size); +} + + +static void +ngx_http_gunzip_filter_free(void *opaque, void *address) +{ +#if 0 + ngx_http_gunzip_ctx_t *ctx = opaque; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ctx->request->connection->log, 0, + "gunzip free: %p", address); +#endif +} + + +static void * +ngx_http_gunzip_create_conf(ngx_conf_t *cf) +{ + ngx_http_gunzip_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_gunzip_conf_t)); + if (conf == NULL) { + return NULL; + } + + /* + * set by ngx_pcalloc(): + * + * conf->bufs.num = 0; + */ + + conf->enable = NGX_CONF_UNSET; + + return conf; +} + + +static char * +ngx_http_gunzip_merge_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_http_gunzip_conf_t *prev = parent; + ngx_http_gunzip_conf_t *conf = child; + + ngx_conf_merge_value(conf->enable, prev->enable, 0); + + ngx_conf_merge_bufs_value(conf->bufs, prev->bufs, + (128 * 1024) / ngx_pagesize, ngx_pagesize); + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_http_gunzip_filter_init(ngx_conf_t *cf) +{ + ngx_http_next_header_filter = ngx_http_top_header_filter; + ngx_http_top_header_filter = ngx_http_gunzip_header_filter; + + ngx_http_next_body_filter = ngx_http_top_body_filter; + ngx_http_top_body_filter = ngx_http_gunzip_body_filter; + + return NGX_OK; +} diff --git a/t/gunzip.t b/t/gunzip.t new file mode 100644 --- /dev/null +++ b/t/gunzip.t @@ -0,0 +1,101 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for gunzip filter module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require IO::Compress::Gzip; }; +Test::More::plan(skip_all => "IO::Compress::Gzip not found") if $@; + +my $t = Test::Nginx->new()->has('--with-http_gzip_static_module')->plan(10); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +master_process off; +daemon off; + +events { +} + +http { + access_log off; + root %%TESTDIR%%; + + client_body_temp_path %%TESTDIR%%/client_body_temp; + fastcgi_temp_path %%TESTDIR%%/fastcgi_temp; + proxy_temp_path %%TESTDIR%%/proxy_temp; + + server { + listen 127.0.0.1:8080; + server_name localhost; + location / { + gunzip on; + gzip_vary on; + proxy_pass http://127.0.0.1:8081/; + proxy_set_header Accept-Encoding gzip; + } + location /error { + error_page 500 /t1; + return 500; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + default_type text/plain; + gzip_static on; + gzip_http_version 1.0; + gzip_types text/plain; + } + } +} + +EOF + +my $in = join('', map { sprintf "X%03dXXXXXX", $_ } (0 .. 99)); +my $out; + +IO::Compress::Gzip::gzip(\$in => \$out); + +$t->write_file('t1.gz', $out); +$t->write_file('t2.gz', $out . $out); +$t->write_file('t3', 'not compressed'); + +$t->run(); + +############################################################################### + +pass('runs'); + +my $t1 = http_get('/t1'); +unlike($t1, qr/Content-Encoding/, 'no content encoding'); +like($t1, qr/^(X\d\d\dXXXXXX){100}$/m, 'correct ungzipped response'); + +like(http_get('/t2'), qr/^(X\d\d\dXXXXXX){200}$/m, 'multiple gzip members'); + +like(http_get('/error'), qr/^(X\d\d\dXXXXXX){100}$/m, 'errors ungzipped'); + +unlike(http_head('/t1'), qr/Content-Encoding/, 'head - no content encoding'); + +like(http_get('/t1'), qr/Vary/, 'get vary'); +like(http_head('/t1'), qr/Vary/, 'head vary'); +unlike(http_get('/t3'), qr/Vary/, 'no vary on non-gzipped get'); +unlike(http_head('/t3'), qr/Vary/, 'no vary on non-gzipped head'); + +###############################################################################