From d1db500cdaab0864a8b118dec89738e858ce0724 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Fri, 4 Mar 2022 16:04:39 +1100 Subject: [PATCH] feat(ext/http): auto-compression of fixed response bodies (#13769) Co-authored-by: Ryan Dahl Co-authored-by: Satya Rohith Co-authored-by: Luca Casonato --- Cargo.lock | 15 + cli/tests/unit/http_test.ts | 503 ++++++++++++++++++++++++++- ext/http/Cargo.toml | 5 + ext/http/compressible.rs | 660 ++++++++++++++++++++++++++++++++++++ ext/http/lib.rs | 163 ++++++++- 5 files changed, 1342 insertions(+), 4 deletions(-) create mode 100644 ext/http/compressible.rs diff --git a/Cargo.lock b/Cargo.lock index caca6cde0d..2208cd7808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -971,10 +971,15 @@ name = "deno_http" version = "0.31.0" dependencies = [ "base64 0.13.0", + "brotli", "bytes", + "cache_control", "deno_core", "deno_websocket", + "flate2", + "fly-accept-encoding", "hyper", + "mime", "percent-encoding", "ring", "serde", @@ -1505,6 +1510,16 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fly-accept-encoding" +version = "0.2.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741d3e4ac3bcebc022cd90e7d1ce376515a73db2d53ba7fd3a7e581d6db7fa97" +dependencies = [ + "http", + "thiserror", +] + [[package]] name = "fnv" version = "1.0.7" diff --git a/cli/tests/unit/http_test.ts b/cli/tests/unit/http_test.ts index 5ff1475b38..68500006f3 100644 --- a/cli/tests/unit/http_test.ts +++ b/cli/tests/unit/http_test.ts @@ -95,7 +95,7 @@ Deno.test( const resp = new Uint8Array(200); const readResult = await conn.read(resp); - assertEquals(readResult, 115); + assertEquals(readResult, 138); conn.close(); @@ -1165,7 +1165,7 @@ Deno.test( const resp = new Uint8Array(200); const readResult = await conn.read(resp); - assertEquals(readResult, 115); + assertEquals(readResult, 138); conn.close(); @@ -1173,6 +1173,505 @@ Deno.test( }, ); +/* Automatic Body Compression */ + +const decoder = new TextDecoder(); + +Deno.test({ + name: "http server compresses body", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const response = new Response( + JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), + { + headers: { "content-type": "application/json" }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept-Encoding\r\n")); + assert(output.includes("content-encoding: gzip\r\n")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server doesn't compress small body", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const response = new Response( + JSON.stringify({ hello: "deno" }), + { + headers: { "content-type": "application/json" }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()).toLocaleLowerCase(); + assert(output.includes("vary: accept-encoding\r\n")); + assert(!output.includes("content-encoding: ")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server respects accept-encoding weights", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals( + request.headers.get("Accept-Encoding"), + "gzip;q=0.8, br;q=1.0, *;q=0.1", + ); + const response = new Response( + JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), + { + headers: { "content-type": "application/json" }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip;q=0.8, br;q=1.0, *;q=0.1", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept-Encoding\r\n")); + assert(output.includes("content-encoding: br\r\n")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server augments vary header", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const response = new Response( + JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), + { + headers: { "content-type": "application/json", vary: "Accept" }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept-Encoding, Accept\r\n")); + assert(output.includes("content-encoding: gzip\r\n")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server weakens etag header", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const response = new Response( + JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), + { + headers: { + "content-type": "application/json", + etag: "33a64df551425fcc55e4d42a148795d9f25f89d4", + }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept-Encoding\r\n")); + assert( + output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"), + ); + assert(output.includes("content-encoding: gzip\r\n")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server passes through weak etag header", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const response = new Response( + JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), + { + headers: { + "content-type": "application/json", + etag: "W/33a64df551425fcc55e4d42a148795d9f25f89d4", + }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept-Encoding\r\n")); + assert( + output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"), + ); + assert(output.includes("content-encoding: gzip\r\n")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server doesn't compress body when no-transform is set", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const response = new Response( + JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), + { + headers: { + "content-type": "application/json", + "cache-control": "no-transform", + }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept-Encoding\r\n")); + assert(!output.includes("content-encoding: ")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server doesn't compress body when content-range is set", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const response = new Response( + JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), + { + headers: { + "content-type": "application/json", + "content-range": "bytes 200-100/67589", + }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept-Encoding\r\n")); + assert(!output.includes("content-encoding: ")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + +Deno.test({ + name: "http server doesn't compress streamed bodies", + permissions: { net: true }, + async fn() { + const hostname = "localhost"; + const port = 4501; + + async function server() { + const encoder = new TextEncoder(); + const listener = Deno.listen({ hostname, port }); + const tcpConn = await listener.accept(); + const httpConn = Deno.serveHttp(tcpConn); + const e = await httpConn.nextRequest(); + assert(e); + const { request, respondWith } = e; + assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); + const bodyInit = new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode( + JSON.stringify({ + hello: "deno", + now: "with", + compressed: "body", + }), + ), + ); + controller.close(); + }, + }); + const response = new Response( + bodyInit, + { + headers: { "content-type": "application/json", vary: "Accept" }, + }, + ); + await respondWith(response); + httpConn.close(); + listener.close(); + } + + async function client() { + const url = `http://${hostname}:${port}/`; + const cmd = [ + "curl", + "-I", + "--request", + "GET", + "--url", + url, + "--header", + "Accept-Encoding: gzip, deflate, br", + ]; + const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); + const status = await proc.status(); + assert(status.success); + const output = decoder.decode(await proc.output()); + assert(output.includes("vary: Accept\r\n")); + assert(!output.includes("content-encoding: ")); + proc.close(); + } + + await Promise.all([server(), client()]); + }, +}); + function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader { // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 const tp = new TextProtoReader(r); diff --git a/ext/http/Cargo.toml b/ext/http/Cargo.toml index dce6a59eb1..58cf210389 100644 --- a/ext/http/Cargo.toml +++ b/ext/http/Cargo.toml @@ -15,10 +15,15 @@ path = "lib.rs" [dependencies] base64 = "0.13.0" +brotli = "3.3.3" bytes = "1" +cache_control = "0.2.0" deno_core = { version = "0.121.0", path = "../../core" } deno_websocket = { version = "0.44.0", path = "../websocket" } +flate2 = "1.0.22" +fly-accept-encoding = "0.2.0-alpha.5" hyper = { version = "0.14.9", features = ["server", "stream", "http1", "http2", "runtime"] } +mime = "0.3.16" percent-encoding = "2.1.0" ring = "0.16.20" serde = { version = "1.0.129", features = ["derive"] } diff --git a/ext/http/compressible.rs b/ext/http/compressible.rs new file mode 100644 index 0000000000..21cd42c762 --- /dev/null +++ b/ext/http/compressible.rs @@ -0,0 +1,660 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use deno_core::ByteString; + +// Data obtained from https://github.com/jshttp/mime-db/blob/fa5e4ef3cc8907ec3c5ec5b85af0c63d7059a5cd/db.json +// Important! Keep this list sorted alphabetically. +const CONTENT_TYPES: &[&str] = &[ + "application/3gpdash-qoe-report+xml", + "application/3gpp-ims+xml", + "application/3gpphal+json", + "application/3gpphalforms+json", + "application/activity+json", + "application/alto-costmap+json", + "application/alto-costmapfilter+json", + "application/alto-directory+json", + "application/alto-endpointcost+json", + "application/alto-endpointcostparams+json", + "application/alto-endpointprop+json", + "application/alto-endpointpropparams+json", + "application/alto-error+json", + "application/alto-networkmap+json", + "application/alto-networkmapfilter+json", + "application/alto-updatestreamcontrol+json", + "application/alto-updatestreamparams+json", + "application/atom+xml", + "application/atomcat+xml", + "application/atomdeleted+xml", + "application/atomsvc+xml", + "application/atsc-dwd+xml", + "application/atsc-held+xml", + "application/atsc-rdt+json", + "application/atsc-rsat+xml", + "application/auth-policy+xml", + "application/beep+xml", + "application/calendar+json", + "application/calendar+xml", + "application/captive+json", + "application/ccmp+xml", + "application/ccxml+xml", + "application/cdfx+xml", + "application/cea-2018+xml", + "application/cellml+xml", + "application/clue+xml", + "application/clue_info+xml", + "application/cnrp+xml", + "application/coap-group+json", + "application/conference-info+xml", + "application/cpl+xml", + "application/csta+xml", + "application/cstadata+xml", + "application/csvm+json", + "application/dart", + "application/dash+xml", + "application/davmount+xml", + "application/dialog-info+xml", + "application/dicom+json", + "application/dicom+xml", + "application/dns+json", + "application/docbook+xml", + "application/dskpp+xml", + "application/dssc+xml", + "application/ecmascript", + "application/elm+json", + "application/elm+xml", + "application/emergencycalldata.cap+xml", + "application/emergencycalldata.comment+xml", + "application/emergencycalldata.control+xml", + "application/emergencycalldata.deviceinfo+xml", + "application/emergencycalldata.providerinfo+xml", + "application/emergencycalldata.serviceinfo+xml", + "application/emergencycalldata.subscriberinfo+xml", + "application/emergencycalldata.veds+xml", + "application/emma+xml", + "application/emotionml+xml", + "application/epp+xml", + "application/expect-ct-report+json", + "application/fdt+xml", + "application/fhir+json", + "application/fhir+xml", + "application/fido.trusted-apps+json", + "application/framework-attributes+xml", + "application/geo+json", + "application/geoxacml+xml", + "application/gml+xml", + "application/gpx+xml", + "application/held+xml", + "application/ibe-key-request+xml", + "application/ibe-pkg-reply+xml", + "application/im-iscomposing+xml", + "application/inkml+xml", + "application/its+xml", + "application/javascript", + "application/jf2feed+json", + "application/jose+json", + "application/jrd+json", + "application/jscalendar+json", + "application/json", + "application/json-patch+json", + "application/jsonml+json", + "application/jwk+json", + "application/jwk-set+json", + "application/kpml-request+xml", + "application/kpml-response+xml", + "application/ld+json", + "application/lgr+xml", + "application/load-control+xml", + "application/lost+xml", + "application/lostsync+xml", + "application/mads+xml", + "application/manifest+json", + "application/marcxml+xml", + "application/mathml+xml", + "application/mathml-content+xml", + "application/mathml-presentation+xml", + "application/mbms-associated-procedure-description+xml", + "application/mbms-deregister+xml", + "application/mbms-envelope+xml", + "application/mbms-msk+xml", + "application/mbms-msk-response+xml", + "application/mbms-protection-description+xml", + "application/mbms-reception-report+xml", + "application/mbms-register+xml", + "application/mbms-register-response+xml", + "application/mbms-schedule+xml", + "application/mbms-user-service-description+xml", + "application/media-policy-dataset+xml", + "application/media_control+xml", + "application/mediaservercontrol+xml", + "application/merge-patch+json", + "application/metalink+xml", + "application/metalink4+xml", + "application/mets+xml", + "application/mmt-aei+xml", + "application/mmt-usd+xml", + "application/mods+xml", + "application/mrb-consumer+xml", + "application/mrb-publish+xml", + "application/msc-ivr+xml", + "application/msc-mixer+xml", + "application/mud+json", + "application/nlsml+xml", + "application/odm+xml", + "application/oebps-package+xml", + "application/omdoc+xml", + "application/opc-nodeset+xml", + "application/p2p-overlay+xml", + "application/patch-ops-error+xml", + "application/pidf+xml", + "application/pidf-diff+xml", + "application/pls+xml", + "application/poc-settings+xml", + "application/postscript", + "application/ppsp-tracker+json", + "application/problem+json", + "application/problem+xml", + "application/provenance+xml", + "application/prs.xsf+xml", + "application/pskc+xml", + "application/pvd+json", + "application/raml+yaml", + "application/rdap+json", + "application/rdf+xml", + "application/reginfo+xml", + "application/reputon+json", + "application/resource-lists+xml", + "application/resource-lists-diff+xml", + "application/rfc+xml", + "application/rlmi+xml", + "application/rls-services+xml", + "application/route-apd+xml", + "application/route-s-tsid+xml", + "application/route-usd+xml", + "application/rsd+xml", + "application/rss+xml", + "application/rtf", + "application/samlassertion+xml", + "application/samlmetadata+xml", + "application/sarif+json", + "application/sarif-external-properties+json", + "application/sbml+xml", + "application/scaip+xml", + "application/scim+json", + "application/senml+json", + "application/senml+xml", + "application/senml-etch+json", + "application/sensml+json", + "application/sensml+xml", + "application/sep+xml", + "application/shf+xml", + "application/simple-filter+xml", + "application/smil+xml", + "application/soap+xml", + "application/sparql-results+xml", + "application/spirits-event+xml", + "application/srgs+xml", + "application/sru+xml", + "application/ssdl+xml", + "application/ssml+xml", + "application/stix+json", + "application/swid+xml", + "application/tar", + "application/taxii+json", + "application/td+json", + "application/tei+xml", + "application/thraud+xml", + "application/tlsrpt+json", + "application/toml", + "application/ttml+xml", + "application/urc-grpsheet+xml", + "application/urc-ressheet+xml", + "application/urc-targetdesc+xml", + "application/urc-uisocketdesc+xml", + "application/vcard+json", + "application/vcard+xml", + "application/vnd.1000minds.decision-model+xml", + "application/vnd.3gpp-prose+xml", + "application/vnd.3gpp-prose-pc3ch+xml", + "application/vnd.3gpp.access-transfer-events+xml", + "application/vnd.3gpp.bsf+xml", + "application/vnd.3gpp.gmop+xml", + "application/vnd.3gpp.mcdata-affiliation-command+xml", + "application/vnd.3gpp.mcdata-info+xml", + "application/vnd.3gpp.mcdata-service-config+xml", + "application/vnd.3gpp.mcdata-ue-config+xml", + "application/vnd.3gpp.mcdata-user-profile+xml", + "application/vnd.3gpp.mcptt-affiliation-command+xml", + "application/vnd.3gpp.mcptt-floor-request+xml", + "application/vnd.3gpp.mcptt-info+xml", + "application/vnd.3gpp.mcptt-location-info+xml", + "application/vnd.3gpp.mcptt-mbms-usage-info+xml", + "application/vnd.3gpp.mcptt-service-config+xml", + "application/vnd.3gpp.mcptt-signed+xml", + "application/vnd.3gpp.mcptt-ue-config+xml", + "application/vnd.3gpp.mcptt-ue-init-config+xml", + "application/vnd.3gpp.mcptt-user-profile+xml", + "application/vnd.3gpp.mcvideo-affiliation-command+xml", + "application/vnd.3gpp.mcvideo-affiliation-info+xml", + "application/vnd.3gpp.mcvideo-info+xml", + "application/vnd.3gpp.mcvideo-location-info+xml", + "application/vnd.3gpp.mcvideo-mbms-usage-info+xml", + "application/vnd.3gpp.mcvideo-service-config+xml", + "application/vnd.3gpp.mcvideo-transmission-request+xml", + "application/vnd.3gpp.mcvideo-ue-config+xml", + "application/vnd.3gpp.mcvideo-user-profile+xml", + "application/vnd.3gpp.mid-call+xml", + "application/vnd.3gpp.sms+xml", + "application/vnd.3gpp.srvcc-ext+xml", + "application/vnd.3gpp.srvcc-info+xml", + "application/vnd.3gpp.state-and-event-info+xml", + "application/vnd.3gpp.ussd+xml", + "application/vnd.3gpp2.bcmcsinfo+xml", + "application/vnd.adobe.xdp+xml", + "application/vnd.amadeus+json", + "application/vnd.amundsen.maze+xml", + "application/vnd.api+json", + "application/vnd.aplextor.warrp+json", + "application/vnd.apothekende.reservation+json", + "application/vnd.apple.installer+xml", + "application/vnd.artisan+json", + "application/vnd.avalon+json", + "application/vnd.avistar+xml", + "application/vnd.balsamiq.bmml+xml", + "application/vnd.bbf.usp.msg+json", + "application/vnd.bekitzur-stech+json", + "application/vnd.biopax.rdf+xml", + "application/vnd.byu.uapi+json", + "application/vnd.capasystems-pg+json", + "application/vnd.chemdraw+xml", + "application/vnd.citationstyles.style+xml", + "application/vnd.collection+json", + "application/vnd.collection.doc+json", + "application/vnd.collection.next+json", + "application/vnd.coreos.ignition+json", + "application/vnd.criticaltools.wbs+xml", + "application/vnd.cryptii.pipe+json", + "application/vnd.ctct.ws+xml", + "application/vnd.cyan.dean.root+xml", + "application/vnd.cyclonedx+json", + "application/vnd.cyclonedx+xml", + "application/vnd.dart", + "application/vnd.datapackage+json", + "application/vnd.dataresource+json", + "application/vnd.dece.ttml+xml", + "application/vnd.dm.delegation+xml", + "application/vnd.document+json", + "application/vnd.drive+json", + "application/vnd.dvb.dvbisl+xml", + "application/vnd.dvb.notif-aggregate-root+xml", + "application/vnd.dvb.notif-container+xml", + "application/vnd.dvb.notif-generic+xml", + "application/vnd.dvb.notif-ia-msglist+xml", + "application/vnd.dvb.notif-ia-registration-request+xml", + "application/vnd.dvb.notif-ia-registration-response+xml", + "application/vnd.dvb.notif-init+xml", + "application/vnd.emclient.accessrequest+xml", + "application/vnd.eprints.data+xml", + "application/vnd.eszigno3+xml", + "application/vnd.etsi.aoc+xml", + "application/vnd.etsi.cug+xml", + "application/vnd.etsi.iptvcommand+xml", + "application/vnd.etsi.iptvdiscovery+xml", + "application/vnd.etsi.iptvprofile+xml", + "application/vnd.etsi.iptvsad-bc+xml", + "application/vnd.etsi.iptvsad-cod+xml", + "application/vnd.etsi.iptvsad-npvr+xml", + "application/vnd.etsi.iptvservice+xml", + "application/vnd.etsi.iptvsync+xml", + "application/vnd.etsi.iptvueprofile+xml", + "application/vnd.etsi.mcid+xml", + "application/vnd.etsi.overload-control-policy-dataset+xml", + "application/vnd.etsi.pstn+xml", + "application/vnd.etsi.sci+xml", + "application/vnd.etsi.simservs+xml", + "application/vnd.etsi.tsl+xml", + "application/vnd.fujifilm.fb.jfi+xml", + "application/vnd.futoin+json", + "application/vnd.gentics.grd+json", + "application/vnd.geo+json", + "application/vnd.geocube+xml", + "application/vnd.google-earth.kml+xml", + "application/vnd.gov.sk.e-form+xml", + "application/vnd.gov.sk.xmldatacontainer+xml", + "application/vnd.hal+json", + "application/vnd.hal+xml", + "application/vnd.handheld-entertainment+xml", + "application/vnd.hc+json", + "application/vnd.heroku+json", + "application/vnd.hyper+json", + "application/vnd.hyper-item+json", + "application/vnd.hyperdrive+json", + "application/vnd.ims.lis.v2.result+json", + "application/vnd.ims.lti.v2.toolconsumerprofile+json", + "application/vnd.ims.lti.v2.toolproxy+json", + "application/vnd.ims.lti.v2.toolproxy.id+json", + "application/vnd.ims.lti.v2.toolsettings+json", + "application/vnd.ims.lti.v2.toolsettings.simple+json", + "application/vnd.informedcontrol.rms+xml", + "application/vnd.infotech.project+xml", + "application/vnd.iptc.g2.catalogitem+xml", + "application/vnd.iptc.g2.conceptitem+xml", + "application/vnd.iptc.g2.knowledgeitem+xml", + "application/vnd.iptc.g2.newsitem+xml", + "application/vnd.iptc.g2.newsmessage+xml", + "application/vnd.iptc.g2.packageitem+xml", + "application/vnd.iptc.g2.planningitem+xml", + "application/vnd.irepository.package+xml", + "application/vnd.las.las+json", + "application/vnd.las.las+xml", + "application/vnd.leap+json", + "application/vnd.liberty-request+xml", + "application/vnd.llamagraphics.life-balance.exchange+xml", + "application/vnd.marlin.drm.actiontoken+xml", + "application/vnd.marlin.drm.conftoken+xml", + "application/vnd.marlin.drm.license+xml", + "application/vnd.mason+json", + "application/vnd.micro+json", + "application/vnd.miele+json", + "application/vnd.mozilla.xul+xml", + "application/vnd.ms-fontobject", + "application/vnd.ms-office.activex+xml", + "application/vnd.ms-opentype", + "application/vnd.ms-playready.initiator+xml", + "application/vnd.ms-printdevicecapabilities+xml", + "application/vnd.ms-printing.printticket+xml", + "application/vnd.ms-printschematicket+xml", + "application/vnd.nearst.inv+json", + "application/vnd.nokia.conml+xml", + "application/vnd.nokia.iptv.config+xml", + "application/vnd.nokia.landmark+xml", + "application/vnd.nokia.landmarkcollection+xml", + "application/vnd.nokia.n-gage.ac+xml", + "application/vnd.nokia.pcd+xml", + "application/vnd.oci.image.manifest.v1+json", + "application/vnd.oftn.l10n+json", + "application/vnd.oipf.contentaccessdownload+xml", + "application/vnd.oipf.contentaccessstreaming+xml", + "application/vnd.oipf.dae.svg+xml", + "application/vnd.oipf.dae.xhtml+xml", + "application/vnd.oipf.mippvcontrolmessage+xml", + "application/vnd.oipf.spdiscovery+xml", + "application/vnd.oipf.spdlist+xml", + "application/vnd.oipf.ueprofile+xml", + "application/vnd.oipf.userprofile+xml", + "application/vnd.oma.bcast.associated-procedure-parameter+xml", + "application/vnd.oma.bcast.drm-trigger+xml", + "application/vnd.oma.bcast.imd+xml", + "application/vnd.oma.bcast.notification+xml", + "application/vnd.oma.bcast.sgdd+xml", + "application/vnd.oma.bcast.smartcard-trigger+xml", + "application/vnd.oma.bcast.sprov+xml", + "application/vnd.oma.cab-address-book+xml", + "application/vnd.oma.cab-feature-handler+xml", + "application/vnd.oma.cab-pcc+xml", + "application/vnd.oma.cab-subs-invite+xml", + "application/vnd.oma.cab-user-prefs+xml", + "application/vnd.oma.dd2+xml", + "application/vnd.oma.drm.risd+xml", + "application/vnd.oma.group-usage-list+xml", + "application/vnd.oma.lwm2m+json", + "application/vnd.oma.pal+xml", + "application/vnd.oma.poc.detailed-progress-report+xml", + "application/vnd.oma.poc.final-report+xml", + "application/vnd.oma.poc.groups+xml", + "application/vnd.oma.poc.invocation-descriptor+xml", + "application/vnd.oma.poc.optimized-progress-report+xml", + "application/vnd.oma.scidm.messages+xml", + "application/vnd.oma.xcap-directory+xml", + "application/vnd.omads-email+xml", + "application/vnd.omads-file+xml", + "application/vnd.omads-folder+xml", + "application/vnd.openblox.game+xml", + "application/vnd.openstreetmap.data+xml", + "application/vnd.openxmlformats-officedocument.custom-properties+xml", + "application/vnd.openxmlformats-officedocument.customxmlproperties+xml", + "application/vnd.openxmlformats-officedocument.drawing+xml", + "application/vnd.openxmlformats-officedocument.drawingml.chart+xml", + "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml", + "application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml", + "application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml", + "application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml", + "application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml", + "application/vnd.openxmlformats-officedocument.extended-properties+xml", + "application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml", + "application/vnd.openxmlformats-officedocument.presentationml.comments+xml", + "application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml", + "application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml", + "application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml", + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml", + "application/vnd.openxmlformats-officedocument.presentationml.presprops+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slide+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml", + "application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml", + "application/vnd.openxmlformats-officedocument.presentationml.tags+xml", + "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml", + "application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", + "application/vnd.openxmlformats-officedocument.theme+xml", + "application/vnd.openxmlformats-officedocument.themeoverride+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml", + "application/vnd.openxmlformats-package.core-properties+xml", + "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml", + "application/vnd.openxmlformats-package.relationships+xml", + "application/vnd.oracle.resource+json", + "application/vnd.otps.ct-kip+xml", + "application/vnd.pagerduty+json", + "application/vnd.poc.group-advertisement+xml", + "application/vnd.pwg-xhtml-print+xml", + "application/vnd.radisys.moml+xml", + "application/vnd.radisys.msml+xml", + "application/vnd.radisys.msml-audit+xml", + "application/vnd.radisys.msml-audit-conf+xml", + "application/vnd.radisys.msml-audit-conn+xml", + "application/vnd.radisys.msml-audit-dialog+xml", + "application/vnd.radisys.msml-audit-stream+xml", + "application/vnd.radisys.msml-conf+xml", + "application/vnd.radisys.msml-dialog+xml", + "application/vnd.radisys.msml-dialog-base+xml", + "application/vnd.radisys.msml-dialog-fax-detect+xml", + "application/vnd.radisys.msml-dialog-fax-sendrecv+xml", + "application/vnd.radisys.msml-dialog-group+xml", + "application/vnd.radisys.msml-dialog-speech+xml", + "application/vnd.radisys.msml-dialog-transform+xml", + "application/vnd.recordare.musicxml+xml", + "application/vnd.restful+json", + "application/vnd.route66.link66+xml", + "application/vnd.seis+json", + "application/vnd.shootproof+json", + "application/vnd.shopkick+json", + "application/vnd.siren+json", + "application/vnd.software602.filler.form+xml", + "application/vnd.solent.sdkm+xml", + "application/vnd.sun.wadl+xml", + "application/vnd.sycle+xml", + "application/vnd.syncml+xml", + "application/vnd.syncml.dm+xml", + "application/vnd.syncml.dmddf+xml", + "application/vnd.syncml.dmtnds+xml", + "application/vnd.tableschema+json", + "application/vnd.think-cell.ppttc+json", + "application/vnd.tmd.mediaflex.api+xml", + "application/vnd.uoml+xml", + "application/vnd.vel+json", + "application/vnd.wv.csp+xml", + "application/vnd.wv.ssp+xml", + "application/vnd.xacml+json", + "application/vnd.xmi+xml", + "application/vnd.yamaha.openscoreformat.osfpvg+xml", + "application/vnd.zzazz.deck+xml", + "application/voicexml+xml", + "application/voucher-cms+json", + "application/wasm", + "application/watcherinfo+xml", + "application/webpush-options+json", + "application/wsdl+xml", + "application/wspolicy+xml", + "application/x-dtbncx+xml", + "application/x-dtbook+xml", + "application/x-dtbresource+xml", + "application/x-httpd-php", + "application/x-javascript", + "application/x-ns-proxy-autoconfig", + "application/x-sh", + "application/x-tar", + "application/x-virtualbox-hdd", + "application/x-virtualbox-ova", + "application/x-virtualbox-ovf", + "application/x-virtualbox-vbox", + "application/x-virtualbox-vdi", + "application/x-virtualbox-vhd", + "application/x-virtualbox-vmdk", + "application/x-web-app-manifest+json", + "application/x-www-form-urlencoded", + "application/x-xliff+xml", + "application/xacml+xml", + "application/xaml+xml", + "application/xcap-att+xml", + "application/xcap-caps+xml", + "application/xcap-diff+xml", + "application/xcap-el+xml", + "application/xcap-error+xml", + "application/xcap-ns+xml", + "application/xcon-conference-info+xml", + "application/xcon-conference-info-diff+xml", + "application/xenc+xml", + "application/xhtml+xml", + "application/xhtml-voice+xml", + "application/xliff+xml", + "application/xml", + "application/xml-dtd", + "application/xml-patch+xml", + "application/xmpp+xml", + "application/xop+xml", + "application/xproc+xml", + "application/xslt+xml", + "application/xspf+xml", + "application/xv+xml", + "application/yang-data+json", + "application/yang-data+xml", + "application/yang-patch+json", + "application/yang-patch+xml", + "application/yin+xml", + "font/otf", + "font/ttf", + "image/bmp", + "image/svg+xml", + "image/vnd.adobe.photoshop", + "image/x-icon", + "image/x-ms-bmp", + "message/imdn+xml", + "message/rfc822", + "model/gltf+json", + "model/gltf-binary", + "model/vnd.collada+xml", + "model/vnd.moml+xml", + "model/x3d+xml", + "text/cache-manifest", + "text/calender", + "text/cmd", + "text/css", + "text/csv", + "text/html", + "text/javascript", + "text/jsx", + "text/less", + "text/markdown", + "text/mdx", + "text/n3", + "text/plain", + "text/richtext", + "text/rtf", + "text/tab-separated-values", + "text/uri-list", + "text/vcard", + "text/vtt", + "text/x-gwt-rpc", + "text/x-jquery-tmpl", + "text/x-markdown", + "text/x-org", + "text/x-processing", + "text/x-suse-ymp", + "text/xml", + "text/yaml", + "x-shader/x-fragment", + "x-shader/x-vertex", +]; + +/// Determine if the supplied content type is considered compressible +pub fn is_content_compressible( + maybe_content_type: Option<&ByteString>, +) -> bool { + if let Some(content_type) = maybe_content_type { + if let Ok(content_type) = std::str::from_utf8(content_type.as_ref()) { + if let Ok(content_type) = content_type.parse::() { + return CONTENT_TYPES + .binary_search(&content_type.essence_str()) + .is_ok(); + } + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn content_type_none() { + assert!(!is_content_compressible(None)); + } + + #[test] + fn non_compressible_content_type() { + assert!(!is_content_compressible(Some(&ByteString( + b"application/vnd.deno+json".to_vec() + )))); + } + + #[test] + fn ncompressible_content_type() { + assert!(is_content_compressible(Some(&ByteString( + b"application/json".to_vec() + )))); + } +} diff --git a/ext/http/lib.rs b/ext/http/lib.rs index 312942303a..b70bed4644 100644 --- a/ext/http/lib.rs +++ b/ext/http/lib.rs @@ -1,6 +1,7 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use bytes::Bytes; +use cache_control::CacheControl; use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::futures::channel::mpsc; @@ -34,6 +35,9 @@ use deno_core::ResourceId; use deno_core::StringOrBuffer; use deno_core::ZeroCopyBuf; use deno_websocket::ws_create_server_stream; +use flate2::write::GzEncoder; +use flate2::Compression; +use fly_accept_encoding::Encoding; use hyper::server::conn::Http; use hyper::service::Service; use hyper::Body; @@ -47,6 +51,7 @@ use std::cmp::min; use std::error::Error; use std::future::Future; use std::io; +use std::io::Write; use std::mem::replace; use std::mem::take; use std::pin::Pin; @@ -58,6 +63,8 @@ use tokio::io::AsyncRead; use tokio::io::AsyncWrite; use tokio::task::spawn_local; +mod compressible; + pub fn init() -> Extension { Extension::builder() .js(include_js_files!( @@ -292,6 +299,7 @@ struct HttpStreamResource { conn: Rc, rd: AsyncRefCell, wr: AsyncRefCell, + accept_encoding: RefCell, cancel_handle: CancelHandle, } @@ -305,6 +313,7 @@ impl HttpStreamResource { conn: conn.clone(), rd: HttpRequestReader::Headers(request).into(), wr: HttpResponseWriter::Headers(response_tx).into(), + accept_encoding: RefCell::new(Encoding::Identity), cancel_handle: CancelHandle::new(), } } @@ -381,6 +390,14 @@ async fn op_http_accept( _ => unreachable!(), }; + { + let mut accept_encoding = stream.accept_encoding.borrow_mut(); + *accept_encoding = fly_accept_encoding::parse(request.headers()) + .ok() + .flatten() + .unwrap_or(Encoding::Identity); + } + let method = request.method().to_string(); let headers = req_headers(request); let url = req_url(request, conn.scheme(), conn.addr()); @@ -497,22 +514,164 @@ async fn op_http_write_headers( let mut builder = Response::builder().status(status); + let mut body_compressible = false; + let mut headers_allow_compression = true; + let mut vary_header = None; + let mut etag_header = None; + let mut content_type_header = None; + builder.headers_mut().unwrap().reserve(headers.len()); for (key, value) in &headers { + match &*key.to_ascii_lowercase() { + b"cache-control" => { + if let Ok(value) = std::str::from_utf8(value) { + if let Some(cache_control) = CacheControl::from_value(value) { + // We skip compression if the cache-control header value is set to + // "no-transform" + if cache_control.no_transform { + headers_allow_compression = false; + } + } + } else { + headers_allow_compression = false; + } + } + b"content-range" => { + // we skip compression if the `content-range` header value is set, as it + // indicates the contents of the body were negotiated based directly + // with the user code and we can't compress the response + headers_allow_compression = false; + } + b"content-type" => { + if !value.is_empty() { + content_type_header = Some(value); + } + } + b"content-encoding" => { + // we don't compress if a content-encoding header was provided + headers_allow_compression = false; + } + // we store the values of ETag and Vary and skip adding them for now, as + // we may need to modify or change. + b"etag" => { + if !value.is_empty() { + etag_header = Some(value); + continue; + } + } + b"vary" => { + if !value.is_empty() { + vary_header = Some(value); + continue; + } + } + _ => {} + } builder = builder.header(key.as_ref(), value.as_ref()); } + if headers_allow_compression { + body_compressible = + compressible::is_content_compressible(content_type_header); + } + let body: Response; let new_wr: HttpResponseWriter; match data { Some(data) => { - // If a buffer was passed, we use it to construct a response body. - body = builder.body(data.into_bytes().into())?; + // Set Vary: Accept-Encoding header for direct body response. + // Note: we set the header irrespective of whether or not we compress the + // data to make sure cache services do not serve uncompressed data to + // clients that support compression. + let vary_value = if let Some(value) = vary_header { + if let Ok(value_str) = std::str::from_utf8(value.as_ref()) { + if !value_str.to_lowercase().contains("accept-encoding") { + format!("Accept-Encoding, {}", value_str) + } else { + value_str.to_string() + } + } else { + // the header value wasn't valid UTF8, so it would have been a + // problem anyways, so sending a default header. + "Accept-Encoding".to_string() + } + } else { + "Accept-Encoding".to_string() + }; + builder = builder.header("vary", &vary_value); + + let accepts_compression = matches!( + *stream.accept_encoding.borrow(), + Encoding::Brotli | Encoding::Gzip + ); + + let should_compress = + body_compressible && data.len() > 20 && accepts_compression; + + if should_compress { + // If user provided a ETag header for uncompressed data, we need to + // ensure it is a Weak Etag header ("W/"). + if let Some(value) = etag_header { + if let Ok(value_str) = std::str::from_utf8(value.as_ref()) { + if !value_str.starts_with("W/") { + builder = builder.header("etag", format!("W/{}", value_str)); + } else { + builder = builder.header("etag", value.as_ref()); + } + } else { + builder = builder.header("etag", value.as_ref()); + } + } + + match *stream.accept_encoding.borrow() { + Encoding::Brotli => { + builder = builder.header("content-encoding", "br"); + // quality level 6 is based on google's nginx default value for + // on-the-fly compression + // https://github.com/google/ngx_brotli#brotli_comp_level + // lgwin 22 is equivalent to brotli window size of (2**22)-16 bytes + // (~4MB) + let mut writer = + brotli::CompressorWriter::new(Vec::new(), 4096, 6, 22); + writer.write_all(&data.into_bytes())?; + body = builder.body(writer.into_inner().into())?; + } + _ => { + assert_eq!(*stream.accept_encoding.borrow(), Encoding::Gzip); + builder = builder.header("content-encoding", "gzip"); + // Gzip, after level 1, doesn't produce significant size difference. + // Probably the reason why nginx's default gzip compression level is + // 1. + // https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_comp_level + let mut writer = GzEncoder::new(Vec::new(), Compression::new(1)); + writer.write_all(&data.into_bytes())?; + body = builder.body(writer.finish().unwrap().into())?; + } + } + } else { + if let Some(value) = etag_header { + builder = builder.header("etag", value.as_ref()); + } + // If a buffer was passed, but isn't compressible, we use it to + // construct a response body. + body = builder.body(data.into_bytes().into())?; + } new_wr = HttpResponseWriter::Closed; } None => { // If no buffer was passed, the caller will stream the response body. + + // TODO(@kitsonk) had compression for streamed bodies. + + // Set the user provided ETag & Vary headers for a streaming response + if let Some(value) = etag_header { + builder = builder.header("etag", value.as_ref()); + } + if let Some(value) = vary_header { + builder = builder.header("vary", value.as_ref()); + } + let (body_tx, body_rx) = Body::channel(); body = builder.body(body_rx)?; new_wr = HttpResponseWriter::Body(body_tx);