Skip to content

Commit cd5c520

Browse files
authored
Merge pull request #14 from plotly/14-add-compression
Add support for HTTP compression using gzip
2 parents 05d4fc6 + 08bb621 commit cd5c520

File tree

9 files changed

+192
-35
lines changed

9 files changed

+192
-35
lines changed

.circleci/config.yml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
version: 2
2+
3+
jobs:
4+
5+
test:
6+
working_directory: /root/project/Dash
7+
8+
docker:
9+
- image: julia:latest
10+
11+
steps:
12+
- checkout
13+
14+
- run:
15+
name: ℹ️ CI Context
16+
command: |
17+
echo "TRIGGERER: ${CIRCLE_USERNAME}"
18+
echo "BUILD_NUMBER: ${CIRCLE_BUILD_NUM}"
19+
echo "BUILD_URL: ${CIRCLE_BUILD_URL}"
20+
echo "BRANCH: ${CIRCLE_BRANCH}"
21+
echo "RUNNING JOB: ${CIRCLE_JOB}"
22+
echo "JOB PARALLELISM: ${CIRCLE_NODE_TOTAL}"
23+
echo "CIRCLE_REPOSITORY_URL: ${CIRCLE_REPOSITORY_URL}"
24+
echo $CIRCLE_JOB > circlejob.txt
25+
26+
- run:
27+
name: 🔎 Unit tests
28+
command: |
29+
julia -e 'using Pkg; Pkg.update(); Pkg.add(PackageSpec(path=pwd())); Pkg.build("Dash"); Pkg.test("Dash", coverage=true);'
30+
31+
workflows:
32+
version: 2
33+
build:
34+
jobs:
35+
- "test"

Project.toml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
88
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
99
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
1010
JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3"
11+
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
1112
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
1213
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
1314
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"

src/Dash.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Dash
2-
import HTTP, JSON2
2+
import HTTP, JSON2, CodecZlib
33
using MacroTools
44
include("ComponentPackages.jl")
55
include("ComponentMetas.jl")

src/app.jl

+16-7
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ struct DashConfig
7373
assets_external_path ::Union{String, Nothing}
7474
include_assets_files ::Bool
7575
show_undo_redo ::Bool
76+
compress ::Bool
7677
end
7778

7879
"""
@@ -87,7 +88,7 @@ struct DashApp
8788
config ::DashConfig
8889
layout ::Layout
8990
callbacks ::Dict{Symbol, Callback}
90-
callable_components ::Dict{Symbol, Component}
91+
callable_components ::Dict{Symbol, Component}
9192

9293
DashApp(name::String, config::DashConfig) = new(name, config, Layout(nothing), Dict{Symbol, Callback}(), Dict{Symbol, Component}())
9394

@@ -129,7 +130,8 @@ end
129130
index_string,
130131
assets_external_path,
131132
include_assets_files,
132-
show_undo_redo
133+
show_undo_redo,
134+
compress
133135
)
134136
135137
Construct a dash app
@@ -209,7 +211,10 @@ Construct a dash app
209211
210212
- `show_undo_redo::Bool`: Default ``false``, set to ``true`` to enable undo
211213
and redo buttons for stepping through the history of the app state.
212-
214+
215+
- `compress::Bool`: Default ``true``, controls whether gzip is used to compress
216+
files and data served by HTTP.jl when supported by the client. Set to
217+
``false`` to disable compression completely.
213218
"""
214219
function dash(name::String;
215220
external_stylesheets = ExternalSrcType[],
@@ -227,7 +232,8 @@ function dash(name::String;
227232
index_string = default_index,
228233
assets_external_path = nothing,
229234
include_assets_files = true,
230-
show_undo_redo = false
235+
show_undo_redo = false,
236+
compress = true
231237

232238
)
233239

@@ -250,7 +256,8 @@ function dash(name::String;
250256
index_string,
251257
assets_external_path,
252258
include_assets_files,
253-
show_undo_redo
259+
show_undo_redo,
260+
compress
254261
)
255262

256263
result = DashApp(name, config)
@@ -273,7 +280,8 @@ function dash(layout_maker ::Function, name;
273280
index_string = default_index,
274281
assets_external_path = nothing,
275282
include_assets_files = true,
276-
show_undo_redo = false
283+
show_undo_redo = false,
284+
compress = true
277285
)
278286
result = dash(name,
279287
external_stylesheets=external_stylesheets,
@@ -291,7 +299,8 @@ function dash(layout_maker ::Function, name;
291299
index_string = index_string,
292300
assets_external_path = assets_external_path,
293301
include_assets_files = include_assets_files,
294-
show_undo_redo = show_undo_redo
302+
show_undo_redo = show_undo_redo,
303+
compress = compress
295304
)
296305
layout!(result, layout_maker())
297306
return result

src/handlers.jl

+43-12
Original file line numberDiff line numberDiff line change
@@ -62,35 +62,64 @@ function process_callback(app::DashApp, body::String)
6262

6363
end
6464

65-
function process_assets(app::DashApp, path)
65+
function make_response(status, headers, body, compress::Bool)
66+
message_headers = headers
67+
68+
if compress
69+
push!(message_headers, "Content-Encoding" => "gzip")
70+
body = transcode(CodecZlib.GzipCompressor, body)
71+
end
72+
73+
response = HTTP.Response(status, body)
74+
for header in message_headers
75+
HTTP.Messages.setheader(response, header)
76+
end
77+
return response
78+
end
79+
80+
function process_assets(app::DashApp, path, compress::Bool)
6681
assets_path = "$(app.config.routes_pathname_prefix)" * strip(app.config.assets_url_path, '/') * "/"
67-
68-
filename = joinpath(app.config.assets_folder, replace(path, assets_path=>""))
82+
filename = joinpath(app.config.assets_folder, replace(path, assets_path=>""))
83+
6984
try
70-
return HTTP.Response(200, [], body = read(filename))
85+
file_contents = read(filename)
86+
mimetype = HTTP.sniff(file_contents)
87+
use_gzip = compress && occursin(r"text|javascript", mimetype)
88+
headers = ["Content-Type" => mimetype]
89+
return make_response(200, headers, file_contents, use_gzip)
7190
catch
7291
return HTTP.Response(404)
7392
end
7493
end
7594

76-
7795
function make_handler(app::DashApp; debug::Bool = false)
7896
index_string::String = index_page(app, debug = debug)
7997

8098
return function (req::HTTP.Request)
99+
body::Union{Nothing, String} = nothing
81100
uri = HTTP.URI(req.target)
101+
102+
# verify that the client accepts compression
103+
accepts_gz = occursin("gzip", HTTP.header(req, "Accept-Encoding"))
104+
# verify that the server was not launched with compress=false
105+
with_gzip = accepts_gz && app.config.compress
106+
107+
headers = []
108+
82109
ComponentPackages.@register_js_sources(uri.path, app.config.routes_pathname_prefix)
83110
if uri.path == "$(app.config.routes_pathname_prefix)"
84-
return HTTP.Response(200, index_string)
111+
body = index_page(app, debug = debug)
85112
end
86113
if uri.path == "$(app.config.routes_pathname_prefix)_dash-layout"
87-
return HTTP.Response(200, ["Content-Type" => "application/json"], body = JSON2.write(app.layout))
114+
body = JSON2.write(app.layout)
115+
push!(headers, "Content-Type" => "application/json")
88116
end
89117
if uri.path == "$(app.config.routes_pathname_prefix)_dash-dependencies"
90-
return HTTP.Response(200, ["Content-Type" => "application/json"], body = dependencies_json(app))
118+
body = dependencies_json(app)
119+
push!(headers, "Content-Type" => "application/json")
91120
end
92121
if startswith(uri.path, "$(app.config.routes_pathname_prefix)assets/")
93-
return process_assets(app, uri.path)
122+
return process_assets(app, uri.path, with_gzip)
94123
end
95124
if uri.path == "$(app.config.routes_pathname_prefix)_dash-update-component" && req.method == "POST"
96125
try
@@ -107,7 +136,9 @@ function make_handler(app::DashApp; debug::Bool = false)
107136
end
108137
end
109138
end
139+
if !isnothing(body)
140+
return make_response(200, headers, body, with_gzip)
141+
end
110142
return HTTP.Response(404)
111-
end
112-
113-
end
143+
end
144+
end

test/Project.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[deps]
2+
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
3+
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
4+
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
5+
Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9"
6+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
7+
JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3"
8+
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
9+
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
10+
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
11+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

test/assets/test.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* Test */

test/config_functional.jl

+14-14
Original file line numberDiff line numberDiff line change
@@ -67,37 +67,37 @@ end
6767

6868
@testset "assets paths" begin
6969
app = dash("test app")
70-
res = Dash.process_assets(app, "/assets/test.png")
70+
res = Dash.process_assets(app, "/assets/test.png", false)
7171
@test res.status == 200
72-
res = Dash.process_assets(app, "/assets/test3.png")
72+
res = Dash.process_assets(app, "/assets/test3.png", false)
7373
@test res.status == 404
74-
res = Dash.process_assets(app, "/images/test.png")
74+
res = Dash.process_assets(app, "/images/test.png", false)
7575
@test res.status == 404
7676

7777
app = dash("test app", url_base_pathname = "/test/")
78-
res = Dash.process_assets(app, "/assets/test.png")
78+
res = Dash.process_assets(app, "/assets/test.png", false)
7979
@test res.status == 404
80-
res = Dash.process_assets(app, "/test/assets/test.png")
80+
res = Dash.process_assets(app, "/test/assets/test.png", false)
8181
@test res.status == 200
82-
res = Dash.process_assets(app, "/images/test.png")
82+
res = Dash.process_assets(app, "/images/test.png", false)
8383
@test res.status == 404
8484

8585
app = dash("test app", assets_url_path = "ass")
86-
res = Dash.process_assets(app, "/ass/test.png")
86+
res = Dash.process_assets(app, "/ass/test.png", false)
8787
@test res.status == 200
88-
res = Dash.process_assets(app, "/ass/test3.png")
88+
res = Dash.process_assets(app, "/ass/test3.png", false)
8989
@test res.status == 404
90-
res = Dash.process_assets(app, "/assets/test3.png")
90+
res = Dash.process_assets(app, "/assets/test3.png", false)
9191
@test res.status == 404
92-
res = Dash.process_assets(app, "/images/test.png")
92+
res = Dash.process_assets(app, "/images/test.png", false)
9393
@test res.status == 404
9494

9595
app = dash("test app", assets_folder = "images")
96-
res = Dash.process_assets(app, "/assets/test.png")
96+
res = Dash.process_assets(app, "/assets/test.png", false)
9797
@test res.status == 404
98-
res = Dash.process_assets(app, "/assets/test_images.png")
98+
res = Dash.process_assets(app, "/assets/test_images.png", false)
9999
@test res.status == 200
100-
res = Dash.process_assets(app, "/images/test.png")
100+
res = Dash.process_assets(app, "/images/test.png", false)
101101
@test res.status == 404
102102
end
103103

@@ -204,4 +204,4 @@ end
204204

205205
index_page = Dash.index_page(app)
206206
@test !isnothing(findfirst("\"show_undo_redo\":true", index_page))
207-
end
207+
end

test/core.jl

+70-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import HTTP, JSON2
22
using Test
33
using Dash
4+
using Inflate
45
@testset "Components" begin
56

67
a_comp = html_a("test", id = "test-a")
@@ -354,4 +355,72 @@ end
354355

355356
@test result[:response][:props][:children] == 10
356357

357-
end
358+
end
359+
360+
@testset "HTTP Compression" begin
361+
# test compression of assets
362+
app = dash("Test app", assets_folder = "assets") do
363+
html_div() do
364+
html_div("test")
365+
end
366+
end
367+
368+
# verify that JSON is not compressed when compress = true
369+
# and Accept-Encoding = "gzip" is not present within request headers
370+
handler = Dash.make_handler(app)
371+
request = HTTP.Request("GET", "/_dash-dependencies")
372+
response = handler(request)
373+
@test app.config.compress == true
374+
@test String(response.body) == "[]"
375+
@test !in("Content-Encoding"=>"gzip", response.headers)
376+
377+
# verify that JSON is compressed when compress = true
378+
# and Accept-Encoding = "gzip" is present within request headers
379+
request = HTTP.Request("GET", "/_dash-dependencies", ["Accept-Encoding"=>"gzip"])
380+
response = handler(request)
381+
@test String(inflate_gzip(response.body)) == "[]"
382+
@test String(response.body) != "[]"
383+
@test in("Content-Encoding"=>"gzip", response.headers)
384+
385+
# ensure no compression of assets when Accept-Encoding not passed
386+
request = HTTP.Request("GET", "/assets/test.css")
387+
response = handler(request)
388+
@test String(response.body) == "/* Test */\n"
389+
@test !in("Content-Encoding"=>"gzip", response.headers)
390+
391+
# ensure compression when Accept-Encoding = "gzip"
392+
request = HTTP.Request("GET", "/assets/test.css", ["Accept-Encoding"=>"gzip"])
393+
response = handler(request)
394+
@test String(inflate_gzip(response.body)) == "/* Test */\n"
395+
@test String(response.body) != "/* Test */\n"
396+
@test in("Content-Encoding"=>"gzip", response.headers)
397+
398+
# test cases for compress = false
399+
app = dash("Test app", assets_folder = "assets", compress=false) do
400+
html_div() do
401+
html_div("test")
402+
end
403+
end
404+
405+
# verify that JSON is not compressed when compress = false
406+
# and Accept-Encoding = "gzip" is not present within request headers
407+
handler = Dash.make_handler(app)
408+
request = HTTP.Request("GET", "/_dash-dependencies")
409+
response = handler(request)
410+
@test app.config.compress == false
411+
@test String(response.body) == "[]"
412+
@test !in("Content-Encoding"=>"gzip", response.headers)
413+
414+
# verify that JSON is NOT compressed when compress = false
415+
# and Accept-Encoding = "gzip" is present within request headers
416+
request = HTTP.Request("GET", "/_dash-dependencies", ["Accept-Encoding"=>"gzip"])
417+
response = handler(request)
418+
@test String(response.body) == "[]"
419+
@test !in("Content-Encoding"=>"gzip", response.headers)
420+
421+
# ensure NO compression when Accept-Encoding = "gzip" and compress = false
422+
request = HTTP.Request("GET", "/assets/test.css", ["Accept-Encoding"=>"gzip"])
423+
response = handler(request)
424+
@test String(response.body) == "/* Test */\n"
425+
@test !in("Content-Encoding"=>"gzip", response.headers)
426+
end

0 commit comments

Comments
 (0)