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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 232e7394f119e448ebf2e16f2064d660ba164c852a23dc6d15c7cd7d3984d4de
4
- data.tar.gz: 27bf2b400b9b4bab65cb2fee22ec75ed0ff74210fb61d53a1932ab3d8c6e9615
3
+ metadata.gz: f049c61bc6afdaebb893a682209ac35e761efa9a4ef6aea5759e276153a4da80
4
+ data.tar.gz: 0334a7f914b12caafa97e7ab00ea3e02845c8451c52c159bbd625ecc9a8f9d51
5
5
  SHA512:
6
- metadata.gz: ab4cf960886ec3f8efdd0587fef2214213d09aabd8501d8c2fb212da706c78c2df73fe9dff7ce0d882945727fd7dc6635650658a5b84421b07257d94c5a2d435
7
- data.tar.gz: 70aea8682c576a672afc7b0f009f03a2ed8fadfe33c5e39e668754d9c6dd3d0e875d40734447d3983d8b11f0b5e92ce275d282766844e2552431476a74837b0f
6
+ metadata.gz: 93188a2d18c3991ac8e1b7306dc26ea552b7094b1683c3c6434ee2b76951c4fa725a280438026cd410ccd1229efd36bbf337173241688e61552a8c9c23f5d07c
7
+ data.tar.gz: e0cc2b44db653d9770facff7958a512bb56da5483b07348bc12af17b76fec7cfc22e95a2d3d5fdcb608edac47591304c04e6ab558e5ae07fff816d125d3e0379
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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 = URI.scheme_list[scheme.upcase] || URI::HTTP
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.to_str)
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-2023, by Samuel Williams.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  require_relative '../internet'
7
- require 'thread/local'
7
+
8
+ ::Thread.attr_accessor :async_http_internet_instance
8
9
 
9
10
  module Async
10
11
  module HTTP
11
12
  class Internet
12
- # Provide access to a shared thread-local instance.
13
- extend ::Thread::Local
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
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
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
- return client.call(request)
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'mock/endpoint'
@@ -2,6 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024, by Thomas Morgan.
5
+ # Copyright, 2024, by Samuel Williams.
5
6
 
6
7
  require_relative 'http1'
7
8
  require_relative 'http2'
@@ -31,7 +31,8 @@ module Async
31
31
  @connection = connection
32
32
  @reason = reason
33
33
 
34
- protocol = headers.delete(UPGRADE)
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 'client'
8
- require_relative 'endpoint'
9
- require_relative 'reference'
7
+ require_relative 'middleware/location_redirector'
10
8
 
11
- require 'protocol/http/middleware'
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
- class TooManyRedirects < StandardError
17
- end
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
@@ -64,7 +64,7 @@ module Async
64
64
  connection&.close
65
65
  end
66
66
 
67
- # @returns [Array(Async::Task)] The task that is running the server.
67
+ # @returns [Async::Task] The task that is running the server.
68
68
  def run
69
69
  Async do |task|
70
70
  @endpoint.accept(&self.method(:accept))
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module HTTP
8
- VERSION = "0.68.0"
8
+ VERSION = "0.70.0"
9
9
  end
10
10
  end
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-2023, by Samuel Williams.
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/) or serve it locally using `bake utopia:project:serve`.
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
- Here is a basic example of a client/server running in the same reactor:
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
- ``` ruby
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.68.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-06-23 00:00:00.000000000 Z
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.26'
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.26'
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