long_body 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/README.md +11 -2
- data/lib/long_body.rb +3 -0
- data/lib/long_body/hijack_handler.rb +0 -6
- data/lib/long_body/version.rb +1 -1
- data/long_body.gemspec +3 -2
- data/spec/long_body_spec.rb +1 -0
- data/spec/shared_webserver_examples.rb +36 -4
- data/spec/streaming_app.ru +19 -0
- data/spec/test_download.rb +1 -22
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5900eac4ab0756b862af488203a3e5b431a2956
|
4
|
+
data.tar.gz: 2d2cd54282e2eff1bb687a7dcfa5e969f9acf505
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9651bc656fdfdf9606adbb26f658befbfd5d7c89a1bc199f7d651ff0eff8b0fffcb66288470be42e79c176b3984cedcdcb3dfa4a9c829f131f1a80be3355d45
|
7
|
+
data.tar.gz: 90098d028bb0427d0818e658067a4c40f7dd184421e398db1fa4518fdd9beae1c590ddcb7097775a65f6ad7ae778a826a2653e938adde376a76613a93b0ef341
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# long_body
|
2
2
|
|
3
|
+
[](https://travis-ci.org/julik/long_body)
|
4
|
+
|
3
5
|
Universal Rack middleware for immediate (after-headers) streaming of long Rack response bodies.
|
4
6
|
Normally most Rack handler webservers will buffer your entire response, or buffer your response
|
5
7
|
without telling you for some time before sending it.
|
@@ -43,11 +45,18 @@ Streaming a large file, without buffering:
|
|
43
45
|
[200, h, File.open(s, 'rb')]
|
44
46
|
}
|
45
47
|
|
48
|
+
## Selective bypass
|
49
|
+
|
50
|
+
Most requests in your application (assets, HTML pages and so on) probably do not need this and are better to be sent as-is.
|
51
|
+
Also, such processing will likely bypass all HTTP caching you set up. `long_body` is "always on" by default. To bypass it,
|
52
|
+
send `X-Rack-Long-Body-Skip` header with any truthy contents in your response headers (better use a string value so that
|
53
|
+
`Rack::Lint` does not complain).
|
54
|
+
|
46
55
|
## Compatibility
|
47
56
|
|
48
|
-
This gem is tested on Ruby 2.2, and should run acceptably well on Ruby 2.+. If you are using Thin
|
57
|
+
This gem is tested on Ruby 2.2, and should run acceptably well on Ruby 2.+. If you are using Thin it is recommended to
|
49
58
|
use Ruby 2.+ because of the fiber stack size limitation. If you are using Puma, Rainbows or other threaded
|
50
|
-
server running this gem on 1.9.
|
59
|
+
server running this gem on 1.9.3+ should be possible as well.
|
51
60
|
|
52
61
|
<table>
|
53
62
|
<tr><th>Webserver</th><th>Version tested</th><th>Compatibility</th></tr>
|
data/lib/long_body.rb
CHANGED
@@ -46,6 +46,9 @@ class LongBody
|
|
46
46
|
# Call the upstream first
|
47
47
|
s, h, b = @app.call(env)
|
48
48
|
|
49
|
+
# Return as-is if the long body skip is requested
|
50
|
+
return [s, h,b] if h.delete('X-Rack-Long-Body-Skip')
|
51
|
+
|
49
52
|
# If the response has nothing to do with the streaming response, just
|
50
53
|
# let it go through as it is not big enough to bother. Also if there is no hijack
|
51
54
|
# support there is no sense to bother at all.
|
@@ -7,12 +7,6 @@
|
|
7
7
|
# For more on this:
|
8
8
|
# http://apidock.com/ruby/IO/write_nonblock
|
9
9
|
# http://old.blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/
|
10
|
-
#
|
11
|
-
# By using this class as a middleware you will put a select() wait
|
12
|
-
# spinlock on the output socket of the webserver.
|
13
|
-
#
|
14
|
-
# The middleware will only trigger for 200 and 206 responses, and only
|
15
|
-
# if the Rack handler it is running on supports rack hijacking.
|
16
10
|
module LongBody::HijackHandler
|
17
11
|
extend self
|
18
12
|
HIJACK_HEADER = 'rack.hijack'.freeze
|
data/lib/long_body/version.rb
CHANGED
data/long_body.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: long_body 0.0.
|
5
|
+
# stub: long_body 0.0.2 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "long_body"
|
9
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.2"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.files = [
|
22
22
|
".document",
|
23
23
|
".rspec",
|
24
|
+
".travis.yml",
|
24
25
|
"Gemfile",
|
25
26
|
"LICENSE.txt",
|
26
27
|
"README.md",
|
data/spec/long_body_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
RSpec.shared_examples "compliant" do
|
2
2
|
it "when the response is sent in full works with predefined Content-Length" do | example |
|
3
|
-
parts = TestDownload.perform("http://0.0.0.0
|
3
|
+
parts = TestDownload.perform("http://0.0.0.0:#{port}/with-content-length")
|
4
4
|
timings = parts.map(&:time_difference)
|
5
5
|
|
6
6
|
# $postrun.puts ""
|
@@ -22,7 +22,7 @@ RSpec.shared_examples "compliant" do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'when the response is sent in full works with chunked encoding' do | example |
|
25
|
-
parts = TestDownload.perform("http://0.0.0.0
|
25
|
+
parts = TestDownload.perform("http://0.0.0.0:#{port}/chunked")
|
26
26
|
timings = parts.map(&:time_difference)
|
27
27
|
|
28
28
|
# $postrun.puts example.full_description
|
@@ -40,18 +40,50 @@ RSpec.shared_examples "compliant" do
|
|
40
40
|
expect(received_after_previous).to be_within(1).of(0.3)
|
41
41
|
end
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
|
+
it 'bypasses when X-Rack-Long-Body-Skip is set' do | example |
|
45
|
+
parts = TestDownload.perform("http://0.0.0.0:#{port}/explicitly-skipping-long-body")
|
46
|
+
timings = parts.map(&:time_difference)
|
47
|
+
|
48
|
+
# $postrun.puts example.full_description
|
49
|
+
# $postrun.puts "Part receive timings: #{timings.inspect}"
|
50
|
+
# $postrun.puts ""
|
51
|
+
|
52
|
+
expect(File).to exist('/tmp/streamer_close.mark')
|
53
|
+
|
54
|
+
(1..(parts.length-1)).each do | part_i|
|
55
|
+
this_part = parts[part_i]
|
56
|
+
previous_part = parts[part_i -1]
|
57
|
+
received_after_previous = this_part.time_difference - previous_part.time_difference
|
58
|
+
|
59
|
+
# Ensure there was some time before this chunk arrived. This is the most important test.
|
60
|
+
expect(received_after_previous).to be_within(1).of(0.3)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
it 'raises an error if no Content-Length or chunked transfer encoding is set' do
|
66
|
+
parts = TestDownload.perform("http://0.0.0.0:#{port}/error-with-unclassified-body")
|
67
|
+
response_body = parts.join
|
68
|
+
expect(response_body).to include('If uncertain, insert Rack')
|
69
|
+
end
|
70
|
+
|
44
71
|
it 'when the HTTP client process is killed midflight, does not read more chunks from the body object' do
|
45
72
|
# This test checks whether the server makes the iterable body complete if the client closes the connection
|
46
73
|
# prematurely. If you have the callbacks set up wrong on Thin, for instance, it will read the response
|
47
74
|
# completely and potentially buffer it in memory, filling up your RAM. We need to ensure that the server
|
48
75
|
# uses it's internal mechanics to stop reading the body once the client is dropped.
|
49
76
|
pid = fork do
|
50
|
-
TestDownload.perform("http://0.0.0.0
|
77
|
+
TestDownload.perform("http://0.0.0.0:#{port}/with-content-length")
|
51
78
|
end
|
52
79
|
sleep(1)
|
53
80
|
Process.kill("KILL", pid)
|
54
81
|
|
82
|
+
# Wait for the webserver to terminate operations on that request (the close()
|
83
|
+
# call on the body is usually not immediate, but gets executed once the write
|
84
|
+
# to the client socket fails).
|
85
|
+
sleep 0.9
|
86
|
+
|
55
87
|
written_parts_list = File.read("/tmp/streamer_messages.log").split("\n")
|
56
88
|
expect(written_parts_list.length).to be < 6
|
57
89
|
|
data/spec/streaming_app.ru
CHANGED
@@ -34,12 +34,31 @@ class StreamerWithLength < Streamer
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
map '/error-with-unclassified-body' do
|
38
|
+
use Rack::ShowExceptions
|
39
|
+
use LongBody
|
40
|
+
run ->(env) { [200, {}, %w( one two three )]}
|
41
|
+
end
|
42
|
+
|
37
43
|
map '/chunked' do
|
38
44
|
use LongBody
|
39
45
|
use Rack::Chunked
|
40
46
|
run Streamer
|
41
47
|
end
|
42
48
|
|
49
|
+
class Skipper < Struct.new(:app)
|
50
|
+
def call(env)
|
51
|
+
s, h, b = app.call(env)
|
52
|
+
[s, h.merge('X-Rack-Long-Body-Skip' => 'yes'), b]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
map '/explicitly-skipping-long-body' do
|
57
|
+
use LongBody
|
58
|
+
use Skipper
|
59
|
+
run Streamer
|
60
|
+
end
|
61
|
+
|
43
62
|
map '/with-content-length' do
|
44
63
|
use LongBody
|
45
64
|
run StreamerWithLength
|
data/spec/test_download.rb
CHANGED
@@ -18,25 +18,4 @@ module TestDownload
|
|
18
18
|
end
|
19
19
|
response_chunks
|
20
20
|
end
|
21
|
-
|
22
|
-
def perform_and_abort_after_3_chunks(uri)
|
23
|
-
response_chunks = []
|
24
|
-
catch :abort do
|
25
|
-
uri = URI(uri.to_s)
|
26
|
-
conn = Net::HTTP.new(uri.host, uri.port)
|
27
|
-
conn.read_timeout = 120 # Might take LONG
|
28
|
-
conn.start do |http|
|
29
|
-
req = Net::HTTP::Get.new(uri.request_uri)
|
30
|
-
before_first = Time.now.to_i
|
31
|
-
http.request(req) do |res|
|
32
|
-
res.read_body do |chunk|
|
33
|
-
diff = Time.now.to_i - before_first
|
34
|
-
response_chunks << Part.new(diff, chunk)
|
35
|
-
throw :abort if response_chunks.length == 3
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
response_chunks
|
41
|
-
end
|
42
|
-
end
|
21
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: long_body
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -186,6 +186,7 @@ extra_rdoc_files:
|
|
186
186
|
files:
|
187
187
|
- ".document"
|
188
188
|
- ".rspec"
|
189
|
+
- ".travis.yml"
|
189
190
|
- Gemfile
|
190
191
|
- LICENSE.txt
|
191
192
|
- README.md
|