Skip to content

Commit 4b28a9d

Browse files
committed
Merge pull request #3 from cemerick/master
"fast-start" the delivery of gzip-compressed bodies, and support flushing of GZIPOutputStreams on JDK7+
2 parents 837e517 + d693dae commit 4b28a9d

File tree

3 files changed

+73
-15
lines changed

3 files changed

+73
-15
lines changed

README.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
## ring-gzip-middleware
22

3-
Gzips [Ring](http://github.com/mmcgrana/ring) responses for user agents which can handle it.
3+
Gzips [Ring](http://github.com/ring-clojure/ring) responses for user agents
4+
which can handle it.
45

56
### Usage
67

78
Apply the Ring middleware function `ring.middleware.gzip/wrap-gzip` to
89
your Ring handler, typically at the top level (i.e. as the last bit of
910
middleware in a `->` form).
1011

11-
1212
### Installation
1313

1414
Add `[amalloy/ring-gzip-middleware "0.1.2"]` to your Leingingen dependencies.
1515

16+
### Compression of seq bodies
17+
18+
In JDK versions <=6, [`java.util.zip.GZIPOutputStream.flush()` does not actually
19+
flush data compressed so
20+
far](http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4813885), which means
21+
that every gzip response must be complete before any bytes hit the wire. While
22+
of marginal importance when compressing static files and other resources that
23+
are consumed in an all-or-nothing manner (i.e. virtually everything that is sent
24+
in a Ring response), lazy sequences are impacted negatively by this. In
25+
particular, long-polling or server-sent event responses backed by lazy
26+
sequences, when gzipped under <=JDK6, must be fully consumed before the client
27+
receives any data at all.
28+
29+
So, _this middleware does not gzip-compress Ring seq response bodies unless the
30+
JDK in use is 7+_, in which case it takes advantage of the new `flush`-ability
31+
of `GZIPOutputStream` there.
32+
1633
### License
1734

1835
Copyright (C) 2010 Michael Stephens and other contributors.

src/ring/middleware/gzip.clj

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,55 @@
11
(ns ring.middleware.gzip
2-
(:require [clojure.java.io :as io])
2+
(:require [clojure.java.io :as io]
3+
clojure.reflect)
34
(:import (java.util.zip GZIPOutputStream)
45
(java.io InputStream
6+
OutputStream
57
Closeable
68
File
79
PipedInputStream
810
PipedOutputStream)))
911

12+
; only available on JDK7
13+
(def ^:private flushable-gzip?
14+
(delay (->> (clojure.reflect/reflect GZIPOutputStream)
15+
:members
16+
(some (comp '#{[java.io.OutputStream boolean]} :parameter-types)))))
17+
18+
; only proxying here so we can specialize io/copy (which ring uses to transfer
19+
; InputStream bodies to the servlet response) for reading from the result of
20+
; piped-gzipped-input-stream
21+
(defn- piped-gzipped-input-stream*
22+
[]
23+
(proxy [PipedInputStream] []))
24+
25+
; exactly the same as do-copy for [InputStream OutputStream], but
26+
; flushes the output on every chunk; this allows gzipped content to start
27+
; flowing to clients ASAP (a reasonable change to ring IMO)
28+
(defmethod @#'io/do-copy [(class (piped-gzipped-input-stream*)) OutputStream]
29+
[^InputStream input ^OutputStream output opts]
30+
(let [buffer (make-array Byte/TYPE (or (:buffer-size opts) 1024))]
31+
(loop []
32+
(let [size (.read input buffer)]
33+
(when (pos? size)
34+
(do (.write output buffer 0 size)
35+
(.flush output)
36+
(recur)))))))
37+
1038
(defn piped-gzipped-input-stream [in]
11-
(let [pipe-in (PipedInputStream.)
39+
(let [pipe-in (piped-gzipped-input-stream*)
1240
pipe-out (PipedOutputStream. pipe-in)]
13-
(future ; new thread to prevent blocking deadlock
14-
(with-open [out (GZIPOutputStream. pipe-out)]
41+
; separate thread to prevent blocking deadlock
42+
(future
43+
(with-open [out (if @flushable-gzip?
44+
(GZIPOutputStream. pipe-out true)
45+
(GZIPOutputStream. pipe-out))]
1546
(if (seq? in)
16-
(doseq [string in] (io/copy (str string) out))
47+
(doseq [string in]
48+
(io/copy (str string) out)
49+
(.flush out))
1750
(io/copy in out)))
1851
(when (instance? Closeable in)
19-
(.close in)))
52+
(.close ^Closeable in)))
2053
pipe-in))
2154

2255
(defn gzipped-response [resp]
@@ -34,7 +67,7 @@
3467
(not (get-in resp [:headers "Content-Encoding"]))
3568
(or
3669
(and (string? body) (> (count body) 200))
37-
(seq? body)
70+
(and (seq? body) @flushable-gzip?)
3871
(instance? InputStream body)
3972
(instance? File body)))
4073
(let [accepts (get-in req [:headers "accept-encoding"] "")
@@ -43,4 +76,4 @@
4376
(match 3))))
4477
(gzipped-response resp)
4578
resp))
46-
resp))))
79+
resp))))

test/ring/middleware/gzip_test.clj

+13-5
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,22 @@
4545
(is (Arrays/equals (unzip (resp :body)) (.getBytes output)))))
4646

4747
(deftest test-string-seq-gzip
48-
(let [app (wrap-gzip (fn [req] {:status 200
49-
:body (->> (partition-all 20 output)
50-
(map (partial apply str)))
48+
(let [seq-body (->> (partition-all 20 output)
49+
(map (partial apply str)))
50+
app (wrap-gzip (fn [req] {:status 200
51+
:body seq-body
5152
:headers {}}))
5253
resp (app (accepting "gzip"))]
5354
(is (= 200 (:status resp)))
54-
(is (= "gzip" (encoding resp)))
55-
(is (Arrays/equals (unzip (resp :body)) (.getBytes output)))))
55+
(if @@#'ring.middleware.gzip/flushable-gzip?
56+
(do
57+
(println "Running on JDK7+, testing gzipping of seq response bodies.")
58+
(is (= "gzip" (encoding resp)))
59+
(is (Arrays/equals (unzip (resp :body)) (.getBytes output))))
60+
(do
61+
(println "Running on <=JDK6, testing non-gzipping of seq response bodies.")
62+
(is (nil? (encoding resp)))
63+
(is (= seq-body (resp :body)))))))
5664

5765
(deftest test-accepts
5866
(doseq [ctype ["gzip" "*" "gzip,deflate" "gzip,deflate,sdch"

0 commit comments

Comments
 (0)