pax_global_header 0000666 0000000 0000000 00000000064 13731260101 0014504 g ustar 00root root 0000000 0000000 52 comment=9130422859db85f7603b482c6533984a8bb90131 ruby-async-http-0.52.5/ 0000775 0000000 0000000 00000000000 13731260101 0014646 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/.editorconfig 0000664 0000000 0000000 00000000064 13731260101 0017323 0 ustar 00root root 0000000 0000000 root = true [*] indent_style = tab indent_size = 2 ruby-async-http-0.52.5/.github/ 0000775 0000000 0000000 00000000000 13731260101 0016206 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/.github/workflows/ 0000775 0000000 0000000 00000000000 13731260101 0020243 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/.github/workflows/development.yml 0000664 0000000 0000000 00000001754 13731260101 0023317 0 ustar 00root root 0000000 0000000 name: Development on: [push, pull_request] jobs: test: runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos ruby: - 2.5 - 2.6 - 2.7 experimental: [false] env: [""] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true steps: - uses: actions/checkout@v2 - uses: ioquatix/setup-ruby@master with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Installing packages (ubuntu) if: matrix.os == 'ubuntu' run: sudo apt-get install apache2-utils - name: Run tests timeout-minutes: 5 run: ${{matrix.env}} bundle exec rspec ruby-async-http-0.52.5/.gitignore 0000664 0000000 0000000 00000000200 13731260101 0016626 0 ustar 00root root 0000000 0000000 .tags /.bundle/ /.yardoc /gems.locked /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ .rspec_status .covered.db /h2spec ruby-async-http-0.52.5/.rspec 0000664 0000000 0000000 00000000067 13731260101 0015766 0 ustar 00root root 0000000 0000000 --format documentation --warnings --require spec_helper ruby-async-http-0.52.5/README.md 0000664 0000000 0000000 00000024657 13731260101 0016143 0 ustar 00root root 0000000 0000000 # Async::HTTP An asynchronous client and server implementation of HTTP/1.0, HTTP/1.1 and HTTP/2 including TLS. Support for streaming requests and responses. Built on top of [async](https://github.com/socketry/async) and [async-io](https://github.com/socketry/async-io). [falcon](https://github.com/socketry/falcon) provides a rack-compatible server. [](https://github.com/socketry/async-http/actions?workflow=Development) ## Installation Add this line to your application's Gemfile: ``` ruby gem 'async-http' ``` And then execute: $ bundle Or install it yourself as: $ gem install async-http ## Usage ### Post JSON data Here is an example showing how to post a data structure as JSON to a remote resource: ``` ruby #!/usr/bin/env ruby require 'json' require 'async' require 'async/http/internet' data = {'life' => 42} Async do # Make a new internet: internet = Async::HTTP::Internet.new # Prepare the request: headers = [['accept', 'application/json']] body = [JSON.dump(data)] # Issues a POST request: response = internet.post("https://httpbin.org/anything", headers, body) # Save the response body to a local file: pp JSON.parse(response.read) ensure # The internet is closed for business: internet.close end ``` Consider using [async-rest](https://github.com/socketry/async-rest) instead. ### Multiple Requests To issue multiple requests concurrently, you should use a barrier, e.g. ``` ruby #!/usr/bin/env ruby require 'async' require 'async/barrier' require 'async/http/internet' TOPICS = ["ruby", "python", "rust"] Async do internet = Async::HTTP::Internet.new barrier = Async::Barrier.new # Spawn an asynchronous task for each topic: TOPICS.each do |topic| barrier.async do response = internet.get "https://www.google.com/search?q=#{topic}" puts "Found #{topic}: #{response.read.scan(topic).size} times." end end # Ensure we wait for all requests to complete before continuing: barrier.wait ensure internet&.close end ``` #### Limiting Requests If you need to limit the number of simultaneous requests, use a semaphore. ``` ruby #!/usr/bin/env ruby require 'async' require 'async/barrier' require 'async/semaphore' require 'async/http/internet' TOPICS = ["ruby", "python", "rust"] Async do internet = Async::HTTP::Internet.new barrier = Async::Barrier.new semaphore = Async::Semaphore.new(2, parent: barrier) # Spawn an asynchronous task for each topic: TOPICS.each do |topic| semaphore.async do response = internet.get "https://www.google.com/search?q=#{topic}" puts "Found #{topic}: #{response.read.scan(topic).size} times." end end # Ensure we wait for all requests to complete before continuing: barrier.wait ensure internet&.close end ``` ### Downloading a File Here is an example showing how to download a file and save it to a local path: ``` ruby #!/usr/bin/env ruby require 'async' require 'async/http/internet' Async do # Make a new internet: internet = Async::HTTP::Internet.new # Issues a GET request to Google: response = internet.get("https://www.google.com/search?q=kittens") # Save the response body to a local file: response.save("/tmp/search.html") ensure # The internet is closed for business: internet.close end ``` ### Basic Client/Server Here is a basic example of a client/server running in the same reactor: ``` ruby #!/usr/bin/env ruby require 'async' require 'async/http/server' require 'async/http/client' require 'async/http/endpoint' require 'async/http/protocol/response' endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:9294') app = lambda do |request| Protocol::HTTP::Response[200, {}, ["Hello World"]] end server = Async::HTTP::Server.new(app, endpoint) client = Async::HTTP::Client.new(endpoint) Async do |task| server_task = task.async do server.run end response = client.get("/") puts response.status puts response.read server_task.stop end ``` ### Advanced Verification You can hook into SSL certificate verification to improve server verification. ``` ruby require 'async' require 'async/http' # These are generated from the certificate chain that the server presented. trusted_fingerprints = { "dac9024f54d8f6df94935fb1732638ca6ad77c13" => true, "e6a3b45b062d509b3382282d196efe97d5956ccb" => true, "07d63f4c05a03f1c306f9941b8ebf57598719ea2" => true, "e8d994f44ff20dc78dbff4e59d7da93900572bbf" => true, } Async do endpoint = Async::HTTP::Endpoint.parse("https://www.codeotaku.com/index") # This is a quick hack/POC: ssl_context = endpoint.ssl_context ssl_context.verify_callback = proc do |verified, store_context| certificate = store_context.current_cert fingerprint = OpenSSL::Digest::SHA1.new(certificate.to_der).to_s if trusted_fingerprints.include? fingerprint true else Async.logger.warn("Untrusted Certificate Fingerprint"){fingerprint} false end end endpoint = endpoint.with(ssl_context: ssl_context) client = Async::HTTP::Client.new(endpoint) response = client.get(endpoint.path) pp response.status, response.headers.fields, response.read end ``` ### Timeouts Here's a basic example with a timeout: ``` ruby #!/usr/bin/env ruby require 'async/http/internet' Async do |task| internet = Async::HTTP::Internet.new # Request will timeout after 2 seconds task.with_timeout(2) do response = internet.get "https://httpbin.org/delay/10" end rescue Async::TimeoutError puts "The request timed out" ensure internet&.close end ``` ## Performance On a 4-core 8-thread i7, running `ab` which uses discrete (non-keep-alive) connections: $ ab -c 8 -t 10 http://127.0.0.1:9294/ This is ApacheBench, Version 2.3 <$Revision: 1757674 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 5000 requests Completed 10000 requests Completed 15000 requests Completed 20000 requests Completed 25000 requests Completed 30000 requests Completed 35000 requests Completed 40000 requests Completed 45000 requests Completed 50000 requests Finished 50000 requests Server Software: Server Hostname: 127.0.0.1 Server Port: 9294 Document Path: / Document Length: 13 bytes Concurrency Level: 8 Time taken for tests: 1.869 seconds Complete requests: 50000 Failed requests: 0 Total transferred: 2450000 bytes HTML transferred: 650000 bytes Requests per second: 26755.55 [#/sec] (mean) Time per request: 0.299 [ms] (mean) Time per request: 0.037 [ms] (mean, across all concurrent requests) Transfer rate: 1280.29 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 0 0 0.2 0 6 Waiting: 0 0 0.2 0 6 Total: 0 0 0.2 0 6 Percentage of the requests served within a certain time (ms) 50% 0 66% 0 75% 0 80% 0 90% 0 95% 1 98% 1 99% 1 100% 6 (longest request) On a 4-core 8-thread i7, running `wrk`, which uses 8 keep-alive connections: $ wrk -c 8 -d 10 -t 8 http://127.0.0.1:9294/ Running 10s test @ http://127.0.0.1:9294/ 8 threads and 8 connections Thread Stats Avg Stdev Max +/- Stdev Latency 217.69us 0.99ms 23.21ms 97.39% Req/Sec 12.18k 1.58k 17.67k 83.21% 974480 requests in 10.10s, 60.41MB read Requests/sec: 96485.00 Transfer/sec: 5.98MB According to these results, the cost of handling connections is quite high, while general throughput seems pretty decent. ## Semantic Model ### Scheme HTTP/1 has an implicit scheme determined by the kind of connection made to the server (either `http` or `https`), while HTTP/2 models this explicitly and the client indicates this in the request using the `:scheme` pseudo-header (typically `https`). To normalize this, `Async::HTTP::Client` and `Async::HTTP::Server` have a default scheme which is used if none is supplied. ### Version HTTP/1 has an explicit version while HTTP/2 does not expose the version in any way. ### Reason HTTP/1 responses contain a reason field which is largely irrelevant. HTTP/2 does not support this field. ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ## See Also - [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency. - [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`. - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets. - [async-rest](https://github.com/socketry/async-rest) — A RESTful resource layer built on top of `async-http`. - [async-http-faraday](https://github.com/socketry/async-http-faraday) — A faraday adapter to use `async-http`. ## License Released under the MIT license. Copyright, 2018, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-async-http-0.52.5/async-http.gemspec 0000664 0000000 0000000 00000001651 13731260101 0020310 0 ustar 00root root 0000000 0000000 require_relative "lib/async/http/version" Gem::Specification.new do |spec| spec.name = "async-http" spec.version = Async::HTTP::VERSION spec.summary = "A HTTP client and server library." spec.authors = ["Samuel Williams"] spec.license = "MIT" spec.homepage = "https://github.com/socketry/async-http" spec.files = Dir.glob('{bake,lib}/**/*', File::FNM_DOTMATCH, base: __dir__) spec.add_dependency "async", "~> 1.25" spec.add_dependency "async-io", "~> 1.28" spec.add_dependency "async-pool", "~> 0.2" spec.add_dependency "protocol-http", "~> 0.20.0" spec.add_dependency "protocol-http1", "~> 0.13.0" spec.add_dependency "protocol-http2", "~> 0.14.0" spec.add_development_dependency "async-container", "~> 0.14" spec.add_development_dependency "async-rspec", "~> 1.10" spec.add_development_dependency "covered" spec.add_development_dependency "rack-test" spec.add_development_dependency "rspec", "~> 3.6" end ruby-async-http-0.52.5/bake.rb 0000664 0000000 0000000 00000000000 13731260101 0016063 0 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/bake/ 0000775 0000000 0000000 00000000000 13731260101 0015550 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/bake/async/ 0000775 0000000 0000000 00000000000 13731260101 0016665 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/bake/async/http.rb 0000664 0000000 0000000 00000004110 13731260101 0020165 0 ustar 00root root 0000000 0000000 # Fetch the specified URL and print the response. # @param url [String] the URL to parse and fetch. # @param method [String] the HTTP method to use. def fetch(url, method:) require 'async/http/internet' require 'kernel/sync' terminal = Console::Terminal.for($stdout) terminal[:request] = terminal.style(:blue, nil, :bold) terminal[:response] = terminal.style(:green, nil, :bold) terminal[:length] = terminal.style(nil, nil, :bold) terminal[:key] = terminal.style(nil, nil, :bold) terminal[:chunk_0] = terminal.style(:blue) terminal[:chunk_1] = terminal.style(:cyan) align = 20 format_body = proc do |body, terminal| if body if length = body.length terminal.print(:body, "body with length ", :length, length, "B") else terminal.print(:body, "body without length") end else terminal.print(:body, "no body") end end.curry Sync do internet = Async::HTTP::Internet.new response = internet.send(method.downcase.to_sym, url) terminal.print_line( :request, method.rjust(align), :reset, ": ", url ) terminal.print_line( :response, "version".rjust(align), :reset, ": ", response.version ) terminal.print_line( :response, "status".rjust(align), :reset, ": ", response.status, ) terminal.print_line( :response, "body".rjust(align), :reset, ": ", format_body[response.body], ) response.headers.each do |key, value| terminal.print_line( :key, key.rjust(align), :reset, ": ", :value, value.inspect ) end if body = response.body index = 0 style = [:chunk_0, :chunk_1] response.body.each do |chunk| terminal.print(style[index % 2], chunk) index += 1 end end response.finish if trailers = response.headers.trailers trailers.each do |key, value| terminal.print_line( :key, key.rjust(align), :reset, ": ", :value, value.inspect ) end end internet.close end end # GET the specified URL and print the response. def get(url) self.fetch(url, method: "GET") end # HEAD the specified URL and print the response. def head(url) self.fetch(url, method: "HEAD") end ruby-async-http-0.52.5/bake/async/http/ 0000775 0000000 0000000 00000000000 13731260101 0017644 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/bake/async/http/h2spec.rb 0000664 0000000 0000000 00000001577 13731260101 0021367 0 ustar 00root root 0000000 0000000 def build # Fetch the code: system "go get github.com/spf13/cobra" system "go get github.com/summerwind/h2spec" # This builds `h2spec` into the current directory system "go build ~/go/src/github.com/summerwind/h2spec/cmd/h2spec/h2spec.go" end def test server do system("./h2spec", "-p", "7272") end end private def server require 'async' require 'async/container' require 'async/http/server' require 'async/io/host_endpoint' endpoint = Async::IO::Endpoint.tcp('127.0.0.1', 7272) container = Async::Container.new Async.logger.info(self){"Starting server..."} container.run(count: 1) do server = Async::HTTP::Server.for(endpoint, Async::HTTP::Protocol::HTTP2, "https") do |request| Protocol::HTTP::Response[200, {'content-type' => 'text/plain'}, ["Hello World"]] end Async do server.run end end yield if block_given? ensure container&.stop end ruby-async-http-0.52.5/examples/ 0000775 0000000 0000000 00000000000 13731260101 0016464 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/examples/compare/ 0000775 0000000 0000000 00000000000 13731260101 0020112 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/examples/compare/Gemfile 0000664 0000000 0000000 00000000137 13731260101 0021406 0 ustar 00root root 0000000 0000000 source "https://rubygems.org" gem "benchmark-ips" gem "async" gem "async-http" gem "httpx" ruby-async-http-0.52.5/examples/compare/benchmark.rb 0000775 0000000 0000000 00000002330 13731260101 0022372 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby require 'benchmark' require 'httpx' require 'async' require 'async/barrier' require 'async/semaphore' require 'async/http/internet' URL = "https://www.codeotaku.com/index" REPEATS = 10 Benchmark.bmbm do |x| x.report("async-http") do Async do internet = Async::HTTP::Internet.new i = 0 while i < REPEATS response = internet.get(URL) response.read i += 1 end ensure internet&.close end end x.report("async-http (pipelined)") do Async do |task| internet = Async::HTTP::Internet.new semaphore = Async::Semaphore.new(100, parent: task) barrier = Async::Barrier.new(parent: semaphore) # Warm up the connection pool... response = internet.get(URL) response.read i = 0 while i < REPEATS barrier.async do response = internet.get(URL) response.read end i += 1 end barrier.wait ensure internet&.close end end x.report("httpx") do i = 0 while i < REPEATS response = HTTPX.get(URL) response.read i += 1 end end x.report("httpx (pipelined)") do urls = [URL] * REPEATS responses = HTTPX.get(*urls) responses.each do |response| response.read end end end ruby-async-http-0.52.5/examples/download/ 0000775 0000000 0000000 00000000000 13731260101 0020273 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/examples/download/chunked.rb 0000775 0000000 0000000 00000004141 13731260101 0022244 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby require 'async' require 'async/clock' require 'async/barrier' require 'async/semaphore' require_relative '../../lib/async/http/endpoint' require_relative '../../lib/async/http/client' Async do url = "https://static.openfoodfacts.org/data/en.openfoodfacts.org.products.csv" endpoint = Async::HTTP::Endpoint.parse(url) client = Async::HTTP::Client.new(endpoint) headers = {'user-agent' => 'curl/7.69.1', 'accept' => '*/*'} file = File.open("products.csv", "w") Async.logger.info(self) {"Saving download to #{Dir.pwd}"} begin response = client.head(endpoint.path, headers) content_length = nil if response.success? unless response.headers['accept-ranges'].include?('bytes') raise "Does not advertise support for accept-ranges: bytes!" end unless content_length = response.body&.length raise "Could not determine length of response!" end end ensure response&.close end Async.logger.info(self) {"Content length: #{content_length/(1024**2)}MiB"} parts = [] offset = 0 chunk_size = 1024*1024 start_time = Async::Clock.now amount = 0 while offset < content_length byte_range_start = offset byte_range_end = [offset + chunk_size, content_length].min parts << (byte_range_start...byte_range_end) offset += chunk_size end Async.logger.info(self) {"Breaking download into #{parts.size} parts..."} semaphore = Async::Semaphore.new(8) barrier = Async::Barrier.new(parent: semaphore) while !parts.empty? barrier.async do part = parts.shift Async.logger.info(self) {"Issuing range request range: bytes=#{part.min}-#{part.max}"} response = client.get(endpoint.path, [ ["range", "bytes=#{part.min}-#{part.max-1}"], *headers ]) if response.success? Async.logger.info(self) {"Got response: #{response}... writing data for #{part}."} written = file.pwrite(response.read, part.min) amount += written duration = Async::Clock.now - start_time Async.logger.info(self) {"Rate: #{((amount.to_f/(1024**2))/duration).round(2)}MiB/s"} end end end barrier.wait ensure client&.close end ruby-async-http-0.52.5/examples/fetch/ 0000775 0000000 0000000 00000000000 13731260101 0017555 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/examples/fetch/Gemfile 0000664 0000000 0000000 00000000031 13731260101 0021042 0 ustar 00root root 0000000 0000000 gem 'rack' gem 'falcon' ruby-async-http-0.52.5/examples/fetch/Gemfile.lock 0000664 0000000 0000000 00000003110 13731260101 0021772 0 ustar 00root root 0000000 0000000 GEM specs: async (1.24.2) console (~> 1.0) nio4r (~> 2.3) timers (~> 4.1) async-container (0.16.2) async (~> 1.0) async-io (~> 1.26) process-group async-http (0.50.5) async (~> 1.23) async-io (~> 1.27.0) async-pool (~> 0.2) protocol-http (~> 0.14.1) protocol-http1 (~> 0.10.0) protocol-http2 (~> 0.11.0) async-http-cache (0.1.2) async-http protocol-http (~> 0.14.4) async-io (1.27.4) async (~> 1.14) async-pool (0.2.0) async (~> 1.8) build-environment (1.13.0) coderay (1.1.2) console (1.8.2) falcon (0.35.6) async (~> 1.13) async-container (~> 0.16.0) async-http (~> 0.50.4) async-http-cache (~> 0.1.0) async-io (~> 1.22) build-environment (~> 1.13) localhost (~> 1.1) process-metrics (~> 0.1.0) rack (>= 1.0) samovar (~> 2.1) ffi (1.12.2) localhost (1.1.6) mapping (1.1.1) method_source (0.9.2) nio4r (2.5.2) process-group (1.2.1) process-terminal (~> 0.2.0) process-metrics (0.1.1) process-terminal (0.2.0) ffi protocol-hpack (1.4.2) protocol-http (0.14.4) protocol-http1 (0.10.2) protocol-http (~> 0.13) protocol-http2 (0.11.1) protocol-hpack (~> 1.4) protocol-http (~> 0.2) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) rack (2.2.2) samovar (2.1.4) console (~> 1.0) mapping (~> 1.0) timers (4.3.0) PLATFORMS ruby DEPENDENCIES falcon pry rack BUNDLED WITH 1.17.3 ruby-async-http-0.52.5/examples/fetch/README.md 0000664 0000000 0000000 00000000125 13731260101 0021032 0 ustar 00root root 0000000 0000000 # Fetch This was an experiment to see how browsers handle bi-directional streaming. ruby-async-http-0.52.5/examples/fetch/config.ru 0000664 0000000 0000000 00000000666 13731260101 0021402 0 ustar 00root root 0000000 0000000 require 'rack' class Echo def initialize(app) @app = app end def call(env) request = Rack::Request.new(env) if request.path_info == "/echo" if output = request.body return [200, {}, output.body] else return [200, {}, ["Hello World?"]] end else return @app.call(env) end end end use Echo use Rack::Static, :urls => [''], :root => 'public', :index => 'index.html' run lambda{|env| [404, {}, []]} ruby-async-http-0.52.5/examples/fetch/public/ 0000775 0000000 0000000 00000000000 13731260101 0021033 5 ustar 00root root 0000000 0000000 ruby-async-http-0.52.5/examples/fetch/public/index.html 0000664 0000000 0000000 00000000455 13731260101 0023034 0 ustar 00root root 0000000 0000000