Skip to content

Commit

Permalink
Merge pull request #14 from plotly/14-add-compression
Browse files Browse the repository at this point in the history
Add support for HTTP compression using gzip
  • Loading branch information
waralex committed Apr 27, 2020
2 parents 05d4fc6 + 08bb621 commit cd5c520
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 35 deletions.
35 changes: 35 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: 2

jobs:

test:
working_directory: /root/project/Dash

docker:
- image: julia:latest

steps:
- checkout

- run:
name: ℹ️ CI Context
command: |
echo "TRIGGERER: ${CIRCLE_USERNAME}"
echo "BUILD_NUMBER: ${CIRCLE_BUILD_NUM}"
echo "BUILD_URL: ${CIRCLE_BUILD_URL}"
echo "BRANCH: ${CIRCLE_BRANCH}"
echo "RUNNING JOB: ${CIRCLE_JOB}"
echo "JOB PARALLELISM: ${CIRCLE_NODE_TOTAL}"
echo "CIRCLE_REPOSITORY_URL: ${CIRCLE_REPOSITORY_URL}"
echo $CIRCLE_JOB > circlejob.txt
- run:
name: 🔎 Unit tests
command: |
julia -e 'using Pkg; Pkg.update(); Pkg.add(PackageSpec(path=pwd())); Pkg.build("Dash"); Pkg.test("Dash", coverage=true);'
workflows:
version: 2
build:
jobs:
- "test"
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3"
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
Expand Down
2 changes: 1 addition & 1 deletion src/Dash.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Dash
import HTTP, JSON2
import HTTP, JSON2, CodecZlib
using MacroTools
include("ComponentPackages.jl")
include("ComponentMetas.jl")
Expand Down
23 changes: 16 additions & 7 deletions src/app.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ struct DashConfig
assets_external_path ::Union{String, Nothing}
include_assets_files ::Bool
show_undo_redo ::Bool
compress ::Bool
end

"""
Expand All @@ -87,7 +88,7 @@ struct DashApp
config ::DashConfig
layout ::Layout
callbacks ::Dict{Symbol, Callback}
callable_components ::Dict{Symbol, Component}
callable_components ::Dict{Symbol, Component}

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

Expand Down Expand Up @@ -129,7 +130,8 @@ end
index_string,
assets_external_path,
include_assets_files,
show_undo_redo
show_undo_redo,
compress
)
Construct a dash app
Expand Down Expand Up @@ -209,7 +211,10 @@ Construct a dash app
- `show_undo_redo::Bool`: Default ``false``, set to ``true`` to enable undo
and redo buttons for stepping through the history of the app state.
- `compress::Bool`: Default ``true``, controls whether gzip is used to compress
files and data served by HTTP.jl when supported by the client. Set to
``false`` to disable compression completely.
"""
function dash(name::String;
external_stylesheets = ExternalSrcType[],
Expand All @@ -227,7 +232,8 @@ function dash(name::String;
index_string = default_index,
assets_external_path = nothing,
include_assets_files = true,
show_undo_redo = false
show_undo_redo = false,
compress = true

)

Expand All @@ -250,7 +256,8 @@ function dash(name::String;
index_string,
assets_external_path,
include_assets_files,
show_undo_redo
show_undo_redo,
compress
)

result = DashApp(name, config)
Expand All @@ -273,7 +280,8 @@ function dash(layout_maker ::Function, name;
index_string = default_index,
assets_external_path = nothing,
include_assets_files = true,
show_undo_redo = false
show_undo_redo = false,
compress = true
)
result = dash(name,
external_stylesheets=external_stylesheets,
Expand All @@ -291,7 +299,8 @@ function dash(layout_maker ::Function, name;
index_string = index_string,
assets_external_path = assets_external_path,
include_assets_files = include_assets_files,
show_undo_redo = show_undo_redo
show_undo_redo = show_undo_redo,
compress = compress
)
layout!(result, layout_maker())
return result
Expand Down
55 changes: 43 additions & 12 deletions src/handlers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -62,35 +62,64 @@ function process_callback(app::DashApp, body::String)

end

function process_assets(app::DashApp, path)
function make_response(status, headers, body, compress::Bool)
message_headers = headers

if compress
push!(message_headers, "Content-Encoding" => "gzip")
body = transcode(CodecZlib.GzipCompressor, body)
end

response = HTTP.Response(status, body)
for header in message_headers
HTTP.Messages.setheader(response, header)
end
return response
end

function process_assets(app::DashApp, path, compress::Bool)
assets_path = "$(app.config.routes_pathname_prefix)" * strip(app.config.assets_url_path, '/') * "/"

filename = joinpath(app.config.assets_folder, replace(path, assets_path=>""))
filename = joinpath(app.config.assets_folder, replace(path, assets_path=>""))

try
return HTTP.Response(200, [], body = read(filename))
file_contents = read(filename)
mimetype = HTTP.sniff(file_contents)
use_gzip = compress && occursin(r"text|javascript", mimetype)
headers = ["Content-Type" => mimetype]
return make_response(200, headers, file_contents, use_gzip)
catch
return HTTP.Response(404)
end
end


function make_handler(app::DashApp; debug::Bool = false)
index_string::String = index_page(app, debug = debug)

return function (req::HTTP.Request)
body::Union{Nothing, String} = nothing
uri = HTTP.URI(req.target)

# verify that the client accepts compression
accepts_gz = occursin("gzip", HTTP.header(req, "Accept-Encoding"))
# verify that the server was not launched with compress=false
with_gzip = accepts_gz && app.config.compress

headers = []

ComponentPackages.@register_js_sources(uri.path, app.config.routes_pathname_prefix)
if uri.path == "$(app.config.routes_pathname_prefix)"
return HTTP.Response(200, index_string)
body = index_page(app, debug = debug)
end
if uri.path == "$(app.config.routes_pathname_prefix)_dash-layout"
return HTTP.Response(200, ["Content-Type" => "application/json"], body = JSON2.write(app.layout))
body = JSON2.write(app.layout)
push!(headers, "Content-Type" => "application/json")
end
if uri.path == "$(app.config.routes_pathname_prefix)_dash-dependencies"
return HTTP.Response(200, ["Content-Type" => "application/json"], body = dependencies_json(app))
body = dependencies_json(app)
push!(headers, "Content-Type" => "application/json")
end
if startswith(uri.path, "$(app.config.routes_pathname_prefix)assets/")
return process_assets(app, uri.path)
return process_assets(app, uri.path, with_gzip)
end
if uri.path == "$(app.config.routes_pathname_prefix)_dash-update-component" && req.method == "POST"
try
Expand All @@ -107,7 +136,9 @@ function make_handler(app::DashApp; debug::Bool = false)
end
end
end
if !isnothing(body)
return make_response(200, headers, body, with_gzip)
end
return HTTP.Response(404)
end

end
end
end
11 changes: 11 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[deps]
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3"
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1 change: 1 addition & 0 deletions test/assets/test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* Test */
28 changes: 14 additions & 14 deletions test/config_functional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,37 +67,37 @@ end

@testset "assets paths" begin
app = dash("test app")
res = Dash.process_assets(app, "/assets/test.png")
res = Dash.process_assets(app, "/assets/test.png", false)
@test res.status == 200
res = Dash.process_assets(app, "/assets/test3.png")
res = Dash.process_assets(app, "/assets/test3.png", false)
@test res.status == 404
res = Dash.process_assets(app, "/images/test.png")
res = Dash.process_assets(app, "/images/test.png", false)
@test res.status == 404

app = dash("test app", url_base_pathname = "/test/")
res = Dash.process_assets(app, "/assets/test.png")
res = Dash.process_assets(app, "/assets/test.png", false)
@test res.status == 404
res = Dash.process_assets(app, "/test/assets/test.png")
res = Dash.process_assets(app, "/test/assets/test.png", false)
@test res.status == 200
res = Dash.process_assets(app, "/images/test.png")
res = Dash.process_assets(app, "/images/test.png", false)
@test res.status == 404

app = dash("test app", assets_url_path = "ass")
res = Dash.process_assets(app, "/ass/test.png")
res = Dash.process_assets(app, "/ass/test.png", false)
@test res.status == 200
res = Dash.process_assets(app, "/ass/test3.png")
res = Dash.process_assets(app, "/ass/test3.png", false)
@test res.status == 404
res = Dash.process_assets(app, "/assets/test3.png")
res = Dash.process_assets(app, "/assets/test3.png", false)
@test res.status == 404
res = Dash.process_assets(app, "/images/test.png")
res = Dash.process_assets(app, "/images/test.png", false)
@test res.status == 404

app = dash("test app", assets_folder = "images")
res = Dash.process_assets(app, "/assets/test.png")
res = Dash.process_assets(app, "/assets/test.png", false)
@test res.status == 404
res = Dash.process_assets(app, "/assets/test_images.png")
res = Dash.process_assets(app, "/assets/test_images.png", false)
@test res.status == 200
res = Dash.process_assets(app, "/images/test.png")
res = Dash.process_assets(app, "/images/test.png", false)
@test res.status == 404
end

Expand Down Expand Up @@ -204,4 +204,4 @@ end

index_page = Dash.index_page(app)
@test !isnothing(findfirst("\"show_undo_redo\":true", index_page))
end
end
71 changes: 70 additions & 1 deletion test/core.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import HTTP, JSON2
using Test
using Dash
using Inflate
@testset "Components" begin

a_comp = html_a("test", id = "test-a")
Expand Down Expand Up @@ -354,4 +355,72 @@ end

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

end
end

@testset "HTTP Compression" begin
# test compression of assets
app = dash("Test app", assets_folder = "assets") do
html_div() do
html_div("test")
end
end

# verify that JSON is not compressed when compress = true
# and Accept-Encoding = "gzip" is not present within request headers
handler = Dash.make_handler(app)
request = HTTP.Request("GET", "/_dash-dependencies")
response = handler(request)
@test app.config.compress == true
@test String(response.body) == "[]"
@test !in("Content-Encoding"=>"gzip", response.headers)

# verify that JSON is compressed when compress = true
# and Accept-Encoding = "gzip" is present within request headers
request = HTTP.Request("GET", "/_dash-dependencies", ["Accept-Encoding"=>"gzip"])
response = handler(request)
@test String(inflate_gzip(response.body)) == "[]"
@test String(response.body) != "[]"
@test in("Content-Encoding"=>"gzip", response.headers)

# ensure no compression of assets when Accept-Encoding not passed
request = HTTP.Request("GET", "/assets/test.css")
response = handler(request)
@test String(response.body) == "/* Test */\n"
@test !in("Content-Encoding"=>"gzip", response.headers)

# ensure compression when Accept-Encoding = "gzip"
request = HTTP.Request("GET", "/assets/test.css", ["Accept-Encoding"=>"gzip"])
response = handler(request)
@test String(inflate_gzip(response.body)) == "/* Test */\n"
@test String(response.body) != "/* Test */\n"
@test in("Content-Encoding"=>"gzip", response.headers)

# test cases for compress = false
app = dash("Test app", assets_folder = "assets", compress=false) do
html_div() do
html_div("test")
end
end

# verify that JSON is not compressed when compress = false
# and Accept-Encoding = "gzip" is not present within request headers
handler = Dash.make_handler(app)
request = HTTP.Request("GET", "/_dash-dependencies")
response = handler(request)
@test app.config.compress == false
@test String(response.body) == "[]"
@test !in("Content-Encoding"=>"gzip", response.headers)

# verify that JSON is NOT compressed when compress = false
# and Accept-Encoding = "gzip" is present within request headers
request = HTTP.Request("GET", "/_dash-dependencies", ["Accept-Encoding"=>"gzip"])
response = handler(request)
@test String(response.body) == "[]"
@test !in("Content-Encoding"=>"gzip", response.headers)

# ensure NO compression when Accept-Encoding = "gzip" and compress = false
request = HTTP.Request("GET", "/assets/test.css", ["Accept-Encoding"=>"gzip"])
response = handler(request)
@test String(response.body) == "/* Test */\n"
@test !in("Content-Encoding"=>"gzip", response.headers)
end

0 comments on commit cd5c520

Please sign in to comment.