async-http 0.68.0 → 0.70.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/async/http/endpoint.rb +17 -2
- data/lib/async/http/internet/instance.rb +15 -4
- data/lib/async/http/internet.rb +14 -5
- data/lib/async/http/middleware/location_redirector.rb +144 -0
- data/lib/async/http/mock/endpoint.rb +53 -0
- data/lib/async/http/mock.rb +6 -0
- data/lib/async/http/protocol/http.rb +1 -0
- data/lib/async/http/protocol/http1/response.rb +2 -1
- data/lib/async/http/protocol/http1/server.rb +3 -3
- data/lib/async/http/relative_location.rb +5 -106
- data/lib/async/http/server.rb +1 -1
- data/lib/async/http/version.rb +1 -1
- data/lib/async/http.rb +3 -1
- data/license.md +2 -1
- data/readme.md +11 -356
- data.tar.gz.sig +0 -0
- metadata +9 -5
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f049c61bc6afdaebb893a682209ac35e761efa9a4ef6aea5759e276153a4da80
|
4
|
+
data.tar.gz: 0334a7f914b12caafa97e7ab00ea3e02845c8451c52c159bbd625ecc9a8f9d51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93188a2d18c3991ac8e1b7306dc26ea552b7094b1683c3c6434ee2b76951c4fa725a280438026cd410ccd1229efd36bbf337173241688e61552a8c9c23f5d07c
|
7
|
+
data.tar.gz: e0cc2b44db653d9770facff7958a512bb56da5483b07348bc12af17b76fec7cfc22e95a2d3d5fdcb608edac47591304c04e6ab558e5ae07fff816d125d3e0379
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/lib/async/http/endpoint.rb
CHANGED
@@ -4,6 +4,8 @@
|
|
4
4
|
# Copyright, 2019-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2021-2022, by Adam Daniels.
|
6
6
|
# Copyright, 2024, by Thomas Morgan.
|
7
|
+
# Copyright, 2024, by Igor Sidorov.
|
8
|
+
# Copyright, 2024, by Hal Brodigan.
|
7
9
|
|
8
10
|
require 'io/endpoint'
|
9
11
|
require 'io/endpoint/host_endpoint'
|
@@ -16,6 +18,13 @@ module Async
|
|
16
18
|
module HTTP
|
17
19
|
# Represents a way to connect to a remote HTTP server.
|
18
20
|
class Endpoint < ::IO::Endpoint::Generic
|
21
|
+
SCHEMES = {
|
22
|
+
'http' => URI::HTTP,
|
23
|
+
'https' => URI::HTTPS,
|
24
|
+
'ws' => URI::WS,
|
25
|
+
'wss' => URI::WSS,
|
26
|
+
}
|
27
|
+
|
19
28
|
def self.parse(string, endpoint = nil, **options)
|
20
29
|
url = URI.parse(string).normalize
|
21
30
|
|
@@ -23,9 +32,15 @@ module Async
|
|
23
32
|
end
|
24
33
|
|
25
34
|
# Construct an endpoint with a specified scheme, hostname, optional path, and options.
|
35
|
+
#
|
36
|
+
# @parameter scheme [String] The scheme to use, e.g. "http" or "https".
|
37
|
+
# @parameter hostname [String] The hostname to connect to (or bind to).
|
38
|
+
# @parameter *options [Hash] Additional options, passed to {#initialize}.
|
26
39
|
def self.for(scheme, hostname, path = "/", **options)
|
27
40
|
# TODO: Consider using URI.for once it becomes available:
|
28
|
-
uri_klass =
|
41
|
+
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
42
|
+
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
43
|
+
end
|
29
44
|
|
30
45
|
self.new(
|
31
46
|
uri_klass.new(scheme, nil, hostname, nil, nil, path, nil, nil, nil).normalize,
|
@@ -39,7 +54,7 @@ module Async
|
|
39
54
|
if url.is_a?(Endpoint)
|
40
55
|
return url
|
41
56
|
else
|
42
|
-
Endpoint.parse(url.
|
57
|
+
Endpoint.parse(url.to_s)
|
43
58
|
end
|
44
59
|
end
|
45
60
|
|
@@ -1,16 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2021-
|
4
|
+
# Copyright, 2021-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative '../internet'
|
7
|
-
|
7
|
+
|
8
|
+
::Thread.attr_accessor :async_http_internet_instance
|
8
9
|
|
9
10
|
module Async
|
10
11
|
module HTTP
|
11
12
|
class Internet
|
12
|
-
#
|
13
|
-
|
13
|
+
# The global instance of the internet.
|
14
|
+
def self.instance
|
15
|
+
::Thread.current.async_http_internet_instance ||= self.new
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
::Protocol::HTTP::Methods.each do |name, verb|
|
20
|
+
define_method(verb.downcase) do |url, headers = nil, body = nil, &block|
|
21
|
+
self.instance.call(verb, url, headers, body, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
14
25
|
end
|
15
26
|
end
|
16
27
|
end
|
data/lib/async/http/internet.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2024, by Samuel Williams.
|
5
|
+
# Copyright, 2024, by Igor Sidorov.
|
5
6
|
|
6
7
|
require_relative 'client'
|
7
8
|
require_relative 'endpoint'
|
@@ -38,7 +39,7 @@ module Async
|
|
38
39
|
# @parameter url [String] The URL to request, e.g. `https://www.codeotaku.com`.
|
39
40
|
# @parameter headers [Hash | Protocol::HTTP::Headers] The headers to send with the request.
|
40
41
|
# @parameter body [String | Protocol::HTTP::Body] The body to send with the request.
|
41
|
-
def call(method, url, headers = nil, body = nil)
|
42
|
+
def call(method, url, headers = nil, body = nil, &block)
|
42
43
|
endpoint = Endpoint[url]
|
43
44
|
client = self.client_for(endpoint)
|
44
45
|
|
@@ -47,7 +48,15 @@ module Async
|
|
47
48
|
|
48
49
|
request = ::Protocol::HTTP::Request.new(endpoint.scheme, endpoint.authority, method, endpoint.path, nil, headers, body)
|
49
50
|
|
50
|
-
|
51
|
+
response = client.call(request)
|
52
|
+
|
53
|
+
return response unless block_given?
|
54
|
+
|
55
|
+
begin
|
56
|
+
yield response
|
57
|
+
ensure
|
58
|
+
response.close
|
59
|
+
end
|
51
60
|
end
|
52
61
|
|
53
62
|
def close
|
@@ -59,8 +68,8 @@ module Async
|
|
59
68
|
end
|
60
69
|
|
61
70
|
::Protocol::HTTP::Methods.each do |name, verb|
|
62
|
-
define_method(verb.downcase) do |url, headers = nil, body = nil|
|
63
|
-
self.call(verb, url, headers, body)
|
71
|
+
define_method(verb.downcase) do |url, headers = nil, body = nil, &block|
|
72
|
+
self.call(verb, url, headers, body, &block)
|
64
73
|
end
|
65
74
|
end
|
66
75
|
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2018-2023, by Samuel Williams.
|
5
|
+
# Copyright, 2019-2020, by Brian Morearty.
|
6
|
+
|
7
|
+
require_relative '../reference'
|
8
|
+
|
9
|
+
require 'protocol/http/middleware'
|
10
|
+
require 'protocol/http/body/rewindable'
|
11
|
+
|
12
|
+
module Async
|
13
|
+
module HTTP
|
14
|
+
module Middleware
|
15
|
+
# A client wrapper which transparently handles redirects to a given maximum number of hops.
|
16
|
+
#
|
17
|
+
# The default implementation will only follow relative locations (i.e. those without a scheme) and will switch to GET if the original request was not a GET.
|
18
|
+
#
|
19
|
+
# The best reference for these semantics is defined by the [Fetch specification](https://fetch.spec.whatwg.org/#http-redirect-fetch).
|
20
|
+
#
|
21
|
+
# | Redirect using GET | Permanent | Temporary |
|
22
|
+
# |:-----------------------------------------:|:---------:|:---------:|
|
23
|
+
# | Allowed | 301 | 302 |
|
24
|
+
# | Preserve original method | 308 | 307 |
|
25
|
+
#
|
26
|
+
# For the specific details of the redirect handling, see:
|
27
|
+
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-2> 301 Moved Permanently.
|
28
|
+
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-3> 302 Found.
|
29
|
+
# - <https://datatracker.ietf.org/doc/html/rfc7538 308 Permanent Redirect.
|
30
|
+
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-7> 307 Temporary Redirect.
|
31
|
+
#
|
32
|
+
class LocationRedirector < ::Protocol::HTTP::Middleware
|
33
|
+
class TooManyRedirects < StandardError
|
34
|
+
end
|
35
|
+
|
36
|
+
# Header keys which should be deleted when changing a request from a POST to a GET as defined by <https://fetch.spec.whatwg.org/#request-body-header-name>.
|
37
|
+
PROHIBITED_GET_HEADERS = [
|
38
|
+
'content-encoding',
|
39
|
+
'content-language',
|
40
|
+
'content-location',
|
41
|
+
'content-type',
|
42
|
+
]
|
43
|
+
|
44
|
+
# maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.
|
45
|
+
def initialize(app, maximum_hops = 3)
|
46
|
+
super(app)
|
47
|
+
|
48
|
+
@maximum_hops = maximum_hops
|
49
|
+
end
|
50
|
+
|
51
|
+
# The maximum number of hops which will limit the number of redirects until an error is thrown.
|
52
|
+
attr :maximum_hops
|
53
|
+
|
54
|
+
def redirect_with_get?(request, response)
|
55
|
+
# We only want to switch to GET if the request method is something other than get, e.g. POST.
|
56
|
+
if request.method != GET
|
57
|
+
# According to the RFC, we should only switch to GET if the response is a 301 or 302:
|
58
|
+
return response.status == 301 || response.status == 302
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Handle a redirect to a relative location.
|
63
|
+
#
|
64
|
+
# @parameter request [Protocol::HTTP::Request] The original request, which you can modify if you want to handle the redirect.
|
65
|
+
# @parameter location [String] The relative location to redirect to.
|
66
|
+
# @returns [Boolean] True if the redirect was handled, false if it was not.
|
67
|
+
def handle_redirect(request, location)
|
68
|
+
uri = URI.parse(location)
|
69
|
+
|
70
|
+
if uri.absolute?
|
71
|
+
return false
|
72
|
+
end
|
73
|
+
|
74
|
+
# Update the path of the request:
|
75
|
+
request.path = Reference[request.path] + location
|
76
|
+
|
77
|
+
# Follow the redirect:
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
|
81
|
+
def call(request)
|
82
|
+
# We don't want to follow redirects for HEAD requests:
|
83
|
+
return super if request.head?
|
84
|
+
|
85
|
+
if body = request.body
|
86
|
+
if body.respond_to?(:rewind)
|
87
|
+
# The request body was already rewindable, so use it as is:
|
88
|
+
body = request.body
|
89
|
+
else
|
90
|
+
# The request body was not rewindable, and we might need to resubmit it if we get a response status of 307 or 308, so make it rewindable:
|
91
|
+
body = ::Protocol::HTTP::Body::Rewindable.new(body)
|
92
|
+
request.body = body
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
hops = 0
|
97
|
+
|
98
|
+
while hops <= @maximum_hops
|
99
|
+
response = super(request)
|
100
|
+
|
101
|
+
if response.redirection?
|
102
|
+
hops += 1
|
103
|
+
|
104
|
+
# Get the redirect location:
|
105
|
+
unless location = response.headers['location']
|
106
|
+
return response
|
107
|
+
end
|
108
|
+
|
109
|
+
response.finish
|
110
|
+
|
111
|
+
unless handle_redirect(request, location)
|
112
|
+
return response
|
113
|
+
end
|
114
|
+
|
115
|
+
# Ensure the request (body) is finished and set to nil before we manipulate the request:
|
116
|
+
request.finish
|
117
|
+
|
118
|
+
if request.method == GET or response.preserve_method?
|
119
|
+
# We (might) need to rewind the body so that it can be submitted again:
|
120
|
+
body&.rewind
|
121
|
+
request.body = body
|
122
|
+
else
|
123
|
+
# We are changing the method to GET:
|
124
|
+
request.method = GET
|
125
|
+
|
126
|
+
# We will no longer be submitting the body:
|
127
|
+
body = nil
|
128
|
+
|
129
|
+
# Remove any headers which are not allowed in a GET request:
|
130
|
+
PROHIBITED_GET_HEADERS.each do |header|
|
131
|
+
request.headers.delete(header)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
else
|
135
|
+
return response
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative '../protocol'
|
7
|
+
|
8
|
+
require 'async/queue'
|
9
|
+
|
10
|
+
module Async
|
11
|
+
module HTTP
|
12
|
+
module Mock
|
13
|
+
# This is an endpoint which bridges a client with a local server.
|
14
|
+
class Endpoint
|
15
|
+
def initialize(protocol = Protocol::HTTP2, scheme = "http", authority = "localhost", queue: Queue.new)
|
16
|
+
@protocol = protocol
|
17
|
+
@scheme = scheme
|
18
|
+
@authority = authority
|
19
|
+
|
20
|
+
@queue = queue
|
21
|
+
end
|
22
|
+
|
23
|
+
attr :protocol
|
24
|
+
attr :scheme
|
25
|
+
attr :authority
|
26
|
+
|
27
|
+
# Processing incoming connections
|
28
|
+
# @yield [::HTTP::Protocol::Request] the requests as they come in.
|
29
|
+
def run(parent: Task.current, &block)
|
30
|
+
while peer = @queue.dequeue
|
31
|
+
server = @protocol.server(peer)
|
32
|
+
|
33
|
+
parent.async do
|
34
|
+
server.each(&block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def connect
|
40
|
+
local, remote = ::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
|
41
|
+
|
42
|
+
@queue.enqueue(remote)
|
43
|
+
|
44
|
+
return local
|
45
|
+
end
|
46
|
+
|
47
|
+
def wrap(endpoint)
|
48
|
+
self.class.new(@protocol, endpoint.scheme, endpoint.authority, queue: @queue)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -31,7 +31,8 @@ module Async
|
|
31
31
|
@connection = connection
|
32
32
|
@reason = reason
|
33
33
|
|
34
|
-
|
34
|
+
# Technically, there should never be more than one value for the upgrade header, but we'll just take the first one to avoid complexity.
|
35
|
+
protocol = headers.delete(UPGRADE)&.first
|
35
36
|
|
36
37
|
super(version, status, headers, body, protocol)
|
37
38
|
end
|
@@ -60,9 +60,9 @@ module Async
|
|
60
60
|
# If a response was generated, send it:
|
61
61
|
if response
|
62
62
|
trailer = response.headers.trailer!
|
63
|
-
|
63
|
+
|
64
64
|
write_response(@version, response.status, response.headers)
|
65
|
-
|
65
|
+
|
66
66
|
# Some operations in this method are long running, that is, it's expected that `body.call(stream)` could literally run indefinitely. In order to facilitate garbage collection, we want to nullify as many local variables before calling the streaming body. This ensures that the garbage collection can clean up as much state as possible during the long running operation, so we don't retain objects that are no longer needed.
|
67
67
|
|
68
68
|
if body and protocol = response.protocol
|
@@ -89,7 +89,7 @@ module Async
|
|
89
89
|
|
90
90
|
write_body(version, body, head, trailer)
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
93
|
# We are done with the body, you shouldn't need to call close on it:
|
94
94
|
body = nil
|
95
95
|
else
|
@@ -4,116 +4,15 @@
|
|
4
4
|
# Copyright, 2018-2023, by Samuel Williams.
|
5
5
|
# Copyright, 2019-2020, by Brian Morearty.
|
6
6
|
|
7
|
-
require_relative '
|
8
|
-
require_relative 'endpoint'
|
9
|
-
require_relative 'reference'
|
7
|
+
require_relative 'middleware/location_redirector'
|
10
8
|
|
11
|
-
|
12
|
-
require 'protocol/http/body/rewindable'
|
9
|
+
warn "`Async::HTTP::RelativeLocation` is deprecated and will be removed in the next release. Please use `Async::HTTP::Middleware::LocationRedirector` instead.", uplevel: 1
|
13
10
|
|
14
11
|
module Async
|
15
12
|
module HTTP
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# A client wrapper which transparently handles both relative and absolute redirects to a given maximum number of hops.
|
20
|
-
#
|
21
|
-
# The best reference for these semantics is defined by the [Fetch specification](https://fetch.spec.whatwg.org/#http-redirect-fetch).
|
22
|
-
#
|
23
|
-
# | Redirect using GET | Permanent | Temporary |
|
24
|
-
# |:-----------------------------------------:|:---------:|:---------:|
|
25
|
-
# | Allowed | 301 | 302 |
|
26
|
-
# | Preserve original method | 308 | 307 |
|
27
|
-
#
|
28
|
-
# For the specific details of the redirect handling, see:
|
29
|
-
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-2> 301 Moved Permanently.
|
30
|
-
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-3> 302 Found.
|
31
|
-
# - <https://datatracker.ietf.org/doc/html/rfc7538 308 Permanent Redirect.
|
32
|
-
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-7> 307 Temporary Redirect.
|
33
|
-
#
|
34
|
-
class RelativeLocation < ::Protocol::HTTP::Middleware
|
35
|
-
# Header keys which should be deleted when changing a request from a POST to a GET as defined by <https://fetch.spec.whatwg.org/#request-body-header-name>.
|
36
|
-
PROHIBITED_GET_HEADERS = [
|
37
|
-
'content-encoding',
|
38
|
-
'content-language',
|
39
|
-
'content-location',
|
40
|
-
'content-type',
|
41
|
-
]
|
42
|
-
|
43
|
-
# maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.
|
44
|
-
def initialize(app, maximum_hops = 3)
|
45
|
-
super(app)
|
46
|
-
|
47
|
-
@maximum_hops = maximum_hops
|
48
|
-
end
|
49
|
-
|
50
|
-
# The maximum number of hops which will limit the number of redirects until an error is thrown.
|
51
|
-
attr :maximum_hops
|
52
|
-
|
53
|
-
def redirect_with_get?(request, response)
|
54
|
-
# We only want to switch to GET if the request method is something other than get, e.g. POST.
|
55
|
-
if request.method != GET
|
56
|
-
# According to the RFC, we should only switch to GET if the response is a 301 or 302:
|
57
|
-
return response.status == 301 || response.status == 302
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def call(request)
|
62
|
-
# We don't want to follow redirects for HEAD requests:
|
63
|
-
return super if request.head?
|
64
|
-
|
65
|
-
if body = request.body
|
66
|
-
# We need to cache the body as it might be submitted multiple times if we get a response status of 307 or 308:
|
67
|
-
body = ::Protocol::HTTP::Body::Rewindable.new(body)
|
68
|
-
request.body = body
|
69
|
-
end
|
70
|
-
|
71
|
-
hops = 0
|
72
|
-
|
73
|
-
while hops <= @maximum_hops
|
74
|
-
response = super(request)
|
75
|
-
|
76
|
-
if response.redirection?
|
77
|
-
hops += 1
|
78
|
-
|
79
|
-
# Get the redirect location:
|
80
|
-
unless location = response.headers['location']
|
81
|
-
return response
|
82
|
-
end
|
83
|
-
|
84
|
-
response.finish
|
85
|
-
|
86
|
-
uri = URI.parse(location)
|
87
|
-
|
88
|
-
if uri.absolute?
|
89
|
-
return response
|
90
|
-
else
|
91
|
-
request.path = Reference[request.path] + location
|
92
|
-
end
|
93
|
-
|
94
|
-
if request.method == GET or response.preserve_method?
|
95
|
-
# We (might) need to rewind the body so that it can be submitted again:
|
96
|
-
body&.rewind
|
97
|
-
else
|
98
|
-
# We are changing the method to GET:
|
99
|
-
request.method = GET
|
100
|
-
|
101
|
-
# Clear the request body:
|
102
|
-
request.finish
|
103
|
-
body = nil
|
104
|
-
|
105
|
-
# Remove any headers which are not allowed in a GET request:
|
106
|
-
PROHIBITED_GET_HEADERS.each do |header|
|
107
|
-
request.headers.delete(header)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
else
|
111
|
-
return response
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!"
|
116
|
-
end
|
13
|
+
module Middleware
|
14
|
+
RelativeLocation = Middleware::LocationRedirector
|
15
|
+
TooManyRedirects = RelativeLocation::TooManyRedirects
|
117
16
|
end
|
118
17
|
end
|
119
18
|
end
|
data/lib/async/http/server.rb
CHANGED
data/lib/async/http/version.rb
CHANGED
data/lib/async/http.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative 'http/version'
|
7
7
|
|
8
8
|
require_relative 'http/client'
|
9
9
|
require_relative 'http/server'
|
10
10
|
|
11
|
+
require_relative 'http/internet'
|
12
|
+
|
11
13
|
require_relative 'http/endpoint'
|
data/license.md
CHANGED
@@ -7,7 +7,7 @@ Copyright, 2019, by Denis Talakevich.
|
|
7
7
|
Copyright, 2019-2020, by Brian Morearty.
|
8
8
|
Copyright, 2019, by Cyril Roelandt.
|
9
9
|
Copyright, 2020, by Stefan Wrobel.
|
10
|
-
Copyright, 2020, by Igor Sidorov.
|
10
|
+
Copyright, 2020-2024, by Igor Sidorov.
|
11
11
|
Copyright, 2020, by Bruno Sutic.
|
12
12
|
Copyright, 2020, by Sam Shadwell.
|
13
13
|
Copyright, 2020, by Orgad Shaneh.
|
@@ -21,6 +21,7 @@ Copyright, 2023-2024, by Thomas Morgan.
|
|
21
21
|
Copyright, 2023, by dependabot[bot].
|
22
22
|
Copyright, 2023, by Josh Huber.
|
23
23
|
Copyright, 2024, by Anton Zhuravsky.
|
24
|
+
Copyright, 2024, by Hal Brodigan.
|
24
25
|
|
25
26
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
26
27
|
of this software and associated documentation files (the "Software"), to deal
|
data/readme.md
CHANGED
@@ -4,358 +4,13 @@ An asynchronous client and server implementation of HTTP/1.0, HTTP/1.1 and HTTP/
|
|
4
4
|
|
5
5
|
[![Development Status](https://github.com/socketry/async-http/workflows/Test/badge.svg)](https://github.com/socketry/async-http/actions?workflow=Test)
|
6
6
|
|
7
|
-
## Installation
|
8
|
-
|
9
|
-
Add this line to your application's Gemfile:
|
10
|
-
|
11
|
-
``` ruby
|
12
|
-
gem 'async-http'
|
13
|
-
```
|
14
|
-
|
15
|
-
And then execute:
|
16
|
-
|
17
|
-
$ bundle
|
18
|
-
|
19
|
-
Or install it yourself as:
|
20
|
-
|
21
|
-
$ gem install async-http
|
22
|
-
|
23
7
|
## Usage
|
24
8
|
|
25
|
-
Please see the [project documentation](https://socketry.github.io/async-http/)
|
26
|
-
|
27
|
-
### Post JSON data
|
28
|
-
|
29
|
-
Here is an example showing how to post a data structure as JSON to a remote resource:
|
30
|
-
|
31
|
-
``` ruby
|
32
|
-
#!/usr/bin/env ruby
|
33
|
-
|
34
|
-
require 'json'
|
35
|
-
require 'async'
|
36
|
-
require 'async/http/internet'
|
37
|
-
|
38
|
-
data = {'life' => 42}
|
39
|
-
|
40
|
-
Async do
|
41
|
-
# Make a new internet:
|
42
|
-
internet = Async::HTTP::Internet.new
|
43
|
-
|
44
|
-
# Prepare the request:
|
45
|
-
headers = [['accept', 'application/json']]
|
46
|
-
body = [JSON.dump(data)]
|
47
|
-
|
48
|
-
# Issues a POST request:
|
49
|
-
response = internet.post("https://httpbin.org/anything", headers, body)
|
50
|
-
|
51
|
-
# Save the response body to a local file:
|
52
|
-
pp JSON.parse(response.read)
|
53
|
-
ensure
|
54
|
-
# The internet is closed for business:
|
55
|
-
internet.close
|
56
|
-
end
|
57
|
-
```
|
58
|
-
|
59
|
-
Consider using [async-rest](https://github.com/socketry/async-rest) instead.
|
60
|
-
|
61
|
-
### Multiple Requests
|
62
|
-
|
63
|
-
To issue multiple requests concurrently, you should use a barrier, e.g.
|
64
|
-
|
65
|
-
``` ruby
|
66
|
-
#!/usr/bin/env ruby
|
67
|
-
|
68
|
-
require 'async'
|
69
|
-
require 'async/barrier'
|
70
|
-
require 'async/http/internet'
|
71
|
-
|
72
|
-
TOPICS = ["ruby", "python", "rust"]
|
73
|
-
|
74
|
-
Async do
|
75
|
-
internet = Async::HTTP::Internet.new
|
76
|
-
barrier = Async::Barrier.new
|
77
|
-
|
78
|
-
# Spawn an asynchronous task for each topic:
|
79
|
-
TOPICS.each do |topic|
|
80
|
-
barrier.async do
|
81
|
-
response = internet.get "https://www.google.com/search?q=#{topic}"
|
82
|
-
puts "Found #{topic}: #{response.read.scan(topic).size} times."
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Ensure we wait for all requests to complete before continuing:
|
87
|
-
barrier.wait
|
88
|
-
ensure
|
89
|
-
internet&.close
|
90
|
-
end
|
91
|
-
```
|
92
|
-
|
93
|
-
#### Limiting Requests
|
94
|
-
|
95
|
-
If you need to limit the number of simultaneous requests, use a semaphore.
|
96
|
-
|
97
|
-
``` ruby
|
98
|
-
#!/usr/bin/env ruby
|
99
|
-
|
100
|
-
require 'async'
|
101
|
-
require 'async/barrier'
|
102
|
-
require 'async/semaphore'
|
103
|
-
require 'async/http/internet'
|
104
|
-
|
105
|
-
TOPICS = ["ruby", "python", "rust"]
|
106
|
-
|
107
|
-
Async do
|
108
|
-
internet = Async::HTTP::Internet.new
|
109
|
-
barrier = Async::Barrier.new
|
110
|
-
semaphore = Async::Semaphore.new(2, parent: barrier)
|
111
|
-
|
112
|
-
# Spawn an asynchronous task for each topic:
|
113
|
-
TOPICS.each do |topic|
|
114
|
-
semaphore.async do
|
115
|
-
response = internet.get "https://www.google.com/search?q=#{topic}"
|
116
|
-
puts "Found #{topic}: #{response.read.scan(topic).size} times."
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
# Ensure we wait for all requests to complete before continuing:
|
121
|
-
barrier.wait
|
122
|
-
ensure
|
123
|
-
internet&.close
|
124
|
-
end
|
125
|
-
```
|
126
|
-
|
127
|
-
### Persistent Connections
|
128
|
-
|
129
|
-
To keep connections alive, install the `thread-local` gem,
|
130
|
-
require `async/http/internet/instance`, and use the `instance`, e.g.
|
131
|
-
|
132
|
-
``` ruby
|
133
|
-
#!/usr/bin/env ruby
|
134
|
-
|
135
|
-
require 'async'
|
136
|
-
require 'async/http/internet/instance'
|
137
|
-
|
138
|
-
Async do
|
139
|
-
internet = Async::HTTP::Internet.instance
|
140
|
-
response = internet.get "https://www.google.com/search?q=test"
|
141
|
-
puts "Found #{response.read.size} results."
|
142
|
-
end
|
143
|
-
```
|
144
|
-
|
145
|
-
### Downloading a File
|
146
|
-
|
147
|
-
Here is an example showing how to download a file and save it to a local path:
|
148
|
-
|
149
|
-
``` ruby
|
150
|
-
#!/usr/bin/env ruby
|
151
|
-
|
152
|
-
require 'async'
|
153
|
-
require 'async/http/internet'
|
154
|
-
|
155
|
-
Async do
|
156
|
-
# Make a new internet:
|
157
|
-
internet = Async::HTTP::Internet.new
|
158
|
-
|
159
|
-
# Issues a GET request to Google:
|
160
|
-
response = internet.get("https://www.google.com/search?q=kittens")
|
161
|
-
|
162
|
-
# Save the response body to a local file:
|
163
|
-
response.save("/tmp/search.html")
|
164
|
-
ensure
|
165
|
-
# The internet is closed for business:
|
166
|
-
internet.close
|
167
|
-
end
|
168
|
-
```
|
169
|
-
|
170
|
-
### Basic Client/Server
|
9
|
+
Please see the [project documentation](https://socketry.github.io/async-http/) for more details.
|
171
10
|
|
172
|
-
|
11
|
+
- [Getting Started](https://socketry.github.io/async-http/guides/getting-started/index) - This guide explains how to get started with `Async::HTTP`.
|
173
12
|
|
174
|
-
|
175
|
-
#!/usr/bin/env ruby
|
176
|
-
|
177
|
-
require 'async'
|
178
|
-
require 'async/http/server'
|
179
|
-
require 'async/http/client'
|
180
|
-
require 'async/http/endpoint'
|
181
|
-
require 'async/http/protocol/response'
|
182
|
-
|
183
|
-
endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:9294')
|
184
|
-
|
185
|
-
app = lambda do |request|
|
186
|
-
Protocol::HTTP::Response[200, {}, ["Hello World"]]
|
187
|
-
end
|
188
|
-
|
189
|
-
server = Async::HTTP::Server.new(app, endpoint)
|
190
|
-
client = Async::HTTP::Client.new(endpoint)
|
191
|
-
|
192
|
-
Async do |task|
|
193
|
-
server_task = task.async do
|
194
|
-
server.run
|
195
|
-
end
|
196
|
-
|
197
|
-
response = client.get("/")
|
198
|
-
|
199
|
-
puts response.status
|
200
|
-
puts response.read
|
201
|
-
|
202
|
-
server_task.stop
|
203
|
-
end
|
204
|
-
```
|
205
|
-
|
206
|
-
### Advanced Verification
|
207
|
-
|
208
|
-
You can hook into SSL certificate verification to improve server verification.
|
209
|
-
|
210
|
-
``` ruby
|
211
|
-
require 'async'
|
212
|
-
require 'async/http'
|
213
|
-
|
214
|
-
# These are generated from the certificate chain that the server presented.
|
215
|
-
trusted_fingerprints = {
|
216
|
-
"dac9024f54d8f6df94935fb1732638ca6ad77c13" => true,
|
217
|
-
"e6a3b45b062d509b3382282d196efe97d5956ccb" => true,
|
218
|
-
"07d63f4c05a03f1c306f9941b8ebf57598719ea2" => true,
|
219
|
-
"e8d994f44ff20dc78dbff4e59d7da93900572bbf" => true,
|
220
|
-
}
|
221
|
-
|
222
|
-
Async do
|
223
|
-
endpoint = Async::HTTP::Endpoint.parse("https://www.codeotaku.com/index")
|
224
|
-
|
225
|
-
# This is a quick hack/POC:
|
226
|
-
ssl_context = endpoint.ssl_context
|
227
|
-
|
228
|
-
ssl_context.verify_callback = proc do |verified, store_context|
|
229
|
-
certificate = store_context.current_cert
|
230
|
-
fingerprint = OpenSSL::Digest::SHA1.new(certificate.to_der).to_s
|
231
|
-
|
232
|
-
if trusted_fingerprints.include? fingerprint
|
233
|
-
true
|
234
|
-
else
|
235
|
-
Console.logger.warn("Untrusted Certificate Fingerprint"){fingerprint}
|
236
|
-
false
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
endpoint = endpoint.with(ssl_context: ssl_context)
|
241
|
-
|
242
|
-
client = Async::HTTP::Client.new(endpoint)
|
243
|
-
|
244
|
-
response = client.get(endpoint.path)
|
245
|
-
|
246
|
-
pp response.status, response.headers.fields, response.read
|
247
|
-
end
|
248
|
-
```
|
249
|
-
|
250
|
-
### Timeouts
|
251
|
-
|
252
|
-
Here's a basic example with a timeout:
|
253
|
-
|
254
|
-
``` ruby
|
255
|
-
#!/usr/bin/env ruby
|
256
|
-
|
257
|
-
require 'async/http/internet'
|
258
|
-
|
259
|
-
Async do |task|
|
260
|
-
internet = Async::HTTP::Internet.new
|
261
|
-
|
262
|
-
# Request will timeout after 2 seconds
|
263
|
-
task.with_timeout(2) do
|
264
|
-
response = internet.get "https://httpbin.org/delay/10"
|
265
|
-
end
|
266
|
-
rescue Async::TimeoutError
|
267
|
-
puts "The request timed out"
|
268
|
-
ensure
|
269
|
-
internet&.close
|
270
|
-
end
|
271
|
-
```
|
272
|
-
|
273
|
-
## Performance
|
274
|
-
|
275
|
-
On a 4-core 8-thread i7, running `ab` which uses discrete (non-keep-alive) connections:
|
276
|
-
|
277
|
-
$ ab -c 8 -t 10 http://127.0.0.1:9294/
|
278
|
-
This is ApacheBench, Version 2.3 <$Revision: 1757674 $>
|
279
|
-
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
280
|
-
Licensed to The Apache Software Foundation, http://www.apache.org/
|
281
|
-
|
282
|
-
Benchmarking 127.0.0.1 (be patient)
|
283
|
-
Completed 5000 requests
|
284
|
-
Completed 10000 requests
|
285
|
-
Completed 15000 requests
|
286
|
-
Completed 20000 requests
|
287
|
-
Completed 25000 requests
|
288
|
-
Completed 30000 requests
|
289
|
-
Completed 35000 requests
|
290
|
-
Completed 40000 requests
|
291
|
-
Completed 45000 requests
|
292
|
-
Completed 50000 requests
|
293
|
-
Finished 50000 requests
|
294
|
-
|
295
|
-
|
296
|
-
Server Software:
|
297
|
-
Server Hostname: 127.0.0.1
|
298
|
-
Server Port: 9294
|
299
|
-
|
300
|
-
Document Path: /
|
301
|
-
Document Length: 13 bytes
|
302
|
-
|
303
|
-
Concurrency Level: 8
|
304
|
-
Time taken for tests: 1.869 seconds
|
305
|
-
Complete requests: 50000
|
306
|
-
Failed requests: 0
|
307
|
-
Total transferred: 2450000 bytes
|
308
|
-
HTML transferred: 650000 bytes
|
309
|
-
Requests per second: 26755.55 [#/sec] (mean)
|
310
|
-
Time per request: 0.299 [ms] (mean)
|
311
|
-
Time per request: 0.037 [ms] (mean, across all concurrent requests)
|
312
|
-
Transfer rate: 1280.29 [Kbytes/sec] received
|
313
|
-
|
314
|
-
Connection Times (ms)
|
315
|
-
min mean[+/-sd] median max
|
316
|
-
Connect: 0 0 0.0 0 0
|
317
|
-
Processing: 0 0 0.2 0 6
|
318
|
-
Waiting: 0 0 0.2 0 6
|
319
|
-
Total: 0 0 0.2 0 6
|
320
|
-
|
321
|
-
Percentage of the requests served within a certain time (ms)
|
322
|
-
50% 0
|
323
|
-
66% 0
|
324
|
-
75% 0
|
325
|
-
80% 0
|
326
|
-
90% 0
|
327
|
-
95% 1
|
328
|
-
98% 1
|
329
|
-
99% 1
|
330
|
-
100% 6 (longest request)
|
331
|
-
|
332
|
-
On a 4-core 8-thread i7, running `wrk`, which uses 8 keep-alive connections:
|
333
|
-
|
334
|
-
$ wrk -c 8 -d 10 -t 8 http://127.0.0.1:9294/
|
335
|
-
Running 10s test @ http://127.0.0.1:9294/
|
336
|
-
8 threads and 8 connections
|
337
|
-
Thread Stats Avg Stdev Max +/- Stdev
|
338
|
-
Latency 217.69us 0.99ms 23.21ms 97.39%
|
339
|
-
Req/Sec 12.18k 1.58k 17.67k 83.21%
|
340
|
-
974480 requests in 10.10s, 60.41MB read
|
341
|
-
Requests/sec: 96485.00
|
342
|
-
Transfer/sec: 5.98MB
|
343
|
-
|
344
|
-
According to these results, the cost of handling connections is quite high, while general throughput seems pretty decent.
|
345
|
-
|
346
|
-
## Semantic Model
|
347
|
-
|
348
|
-
### Scheme
|
349
|
-
|
350
|
-
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.
|
351
|
-
|
352
|
-
### Version
|
353
|
-
|
354
|
-
HTTP/1 has an explicit version while HTTP/2 does not expose the version in any way.
|
355
|
-
|
356
|
-
### Reason
|
357
|
-
|
358
|
-
HTTP/1 responses contain a reason field which is largely irrelevant. HTTP/2 does not support this field.
|
13
|
+
- [Testing](https://socketry.github.io/async-http/guides/testing/index) - This guide explains how to use `Async::HTTP` clients and servers in your tests.
|
359
14
|
|
360
15
|
## Contributing
|
361
16
|
|
@@ -367,14 +22,6 @@ We welcome contributions to this project.
|
|
367
22
|
4. Push to the branch (`git push origin my-new-feature`).
|
368
23
|
5. Create new Pull Request.
|
369
24
|
|
370
|
-
### Developer Certificate of Origin
|
371
|
-
|
372
|
-
This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
|
373
|
-
|
374
|
-
### Contributor Covenant
|
375
|
-
|
376
|
-
This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
|
377
|
-
|
378
25
|
## See Also
|
379
26
|
|
380
27
|
- [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency.
|
@@ -382,3 +29,11 @@ This project is governed by the [Contributor Covenant](https://www.contributor-c
|
|
382
29
|
- [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets.
|
383
30
|
- [async-rest](https://github.com/socketry/async-rest) — A RESTful resource layer built on top of `async-http`.
|
384
31
|
- [async-http-faraday](https://github.com/socketry/async-http-faraday) — A faraday adapter to use `async-http`.
|
32
|
+
|
33
|
+
### Developer Certificate of Origin
|
34
|
+
|
35
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
36
|
+
|
37
|
+
### Community Guidelines
|
38
|
+
|
39
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.70.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -10,11 +10,12 @@ authors:
|
|
10
10
|
- Janko Marohnić
|
11
11
|
- Thomas Morgan
|
12
12
|
- Adam Daniels
|
13
|
+
- Igor Sidorov
|
13
14
|
- Anton Zhuravsky
|
14
15
|
- Cyril Roelandt
|
15
16
|
- Denis Talakevich
|
17
|
+
- Hal Brodigan
|
16
18
|
- Ian Ker-Seymer
|
17
|
-
- Igor Sidorov
|
18
19
|
- Josh Huber
|
19
20
|
- Marco Concetto Rudilosso
|
20
21
|
- Olle Jonsson
|
@@ -57,7 +58,7 @@ cert_chain:
|
|
57
58
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
58
59
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
59
60
|
-----END CERTIFICATE-----
|
60
|
-
date: 2024-
|
61
|
+
date: 2024-08-14 00:00:00.000000000 Z
|
61
62
|
dependencies:
|
62
63
|
- !ruby/object:Gem::Dependency
|
63
64
|
name: async
|
@@ -121,14 +122,14 @@ dependencies:
|
|
121
122
|
requirements:
|
122
123
|
- - "~>"
|
123
124
|
- !ruby/object:Gem::Version
|
124
|
-
version: '0.
|
125
|
+
version: '0.28'
|
125
126
|
type: :runtime
|
126
127
|
prerelease: false
|
127
128
|
version_requirements: !ruby/object:Gem::Requirement
|
128
129
|
requirements:
|
129
130
|
- - "~>"
|
130
131
|
- !ruby/object:Gem::Version
|
131
|
-
version: '0.
|
132
|
+
version: '0.28'
|
132
133
|
- !ruby/object:Gem::Dependency
|
133
134
|
name: protocol-http1
|
134
135
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,6 +191,9 @@ files:
|
|
190
191
|
- lib/async/http/endpoint.rb
|
191
192
|
- lib/async/http/internet.rb
|
192
193
|
- lib/async/http/internet/instance.rb
|
194
|
+
- lib/async/http/middleware/location_redirector.rb
|
195
|
+
- lib/async/http/mock.rb
|
196
|
+
- lib/async/http/mock/endpoint.rb
|
193
197
|
- lib/async/http/protocol.rb
|
194
198
|
- lib/async/http/protocol/http.rb
|
195
199
|
- lib/async/http/protocol/http1.rb
|
metadata.gz.sig
CHANGED
Binary file
|