httpx 0.12.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/doc/release_notes/0_10_1.md +1 -1
- data/doc/release_notes/0_13_0.md +58 -0
- data/doc/release_notes/0_13_1.md +5 -0
- data/doc/release_notes/0_13_2.md +9 -0
- data/doc/release_notes/0_14_0.md +79 -0
- data/doc/release_notes/0_14_1.md +7 -0
- data/lib/httpx.rb +1 -2
- data/lib/httpx/callbacks.rb +12 -3
- data/lib/httpx/chainable.rb +2 -2
- data/lib/httpx/connection.rb +29 -22
- data/lib/httpx/connection/http1.rb +35 -15
- data/lib/httpx/connection/http2.rb +61 -15
- data/lib/httpx/headers.rb +7 -3
- data/lib/httpx/io/ssl.rb +30 -17
- data/lib/httpx/io/tcp.rb +48 -27
- data/lib/httpx/io/udp.rb +31 -7
- data/lib/httpx/io/unix.rb +27 -12
- data/lib/httpx/options.rb +97 -74
- data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
- data/lib/httpx/plugins/aws_sigv4.rb +5 -4
- data/lib/httpx/plugins/basic_authentication.rb +8 -3
- data/lib/httpx/plugins/compression.rb +24 -12
- data/lib/httpx/plugins/compression/brotli.rb +10 -7
- data/lib/httpx/plugins/compression/deflate.rb +6 -5
- data/lib/httpx/plugins/compression/gzip.rb +4 -3
- data/lib/httpx/plugins/cookies.rb +3 -7
- data/lib/httpx/plugins/digest_authentication.rb +5 -5
- data/lib/httpx/plugins/expect.rb +6 -6
- data/lib/httpx/plugins/follow_redirects.rb +4 -4
- data/lib/httpx/plugins/grpc.rb +247 -0
- data/lib/httpx/plugins/grpc/call.rb +62 -0
- data/lib/httpx/plugins/grpc/message.rb +85 -0
- data/lib/httpx/plugins/h2c.rb +43 -58
- data/lib/httpx/plugins/internal_telemetry.rb +1 -1
- data/lib/httpx/plugins/multipart/part.rb +2 -2
- data/lib/httpx/plugins/proxy.rb +3 -7
- data/lib/httpx/plugins/proxy/http.rb +5 -4
- data/lib/httpx/plugins/proxy/ssh.rb +3 -3
- data/lib/httpx/plugins/rate_limiter.rb +1 -1
- data/lib/httpx/plugins/retries.rb +14 -15
- data/lib/httpx/plugins/stream.rb +99 -75
- data/lib/httpx/plugins/upgrade.rb +84 -0
- data/lib/httpx/plugins/upgrade/h2.rb +54 -0
- data/lib/httpx/pool.rb +14 -5
- data/lib/httpx/request.rb +25 -2
- data/lib/httpx/resolver/native.rb +7 -3
- data/lib/httpx/response.rb +9 -5
- data/lib/httpx/session.rb +17 -7
- data/lib/httpx/transcoder/chunker.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/callbacks.rbs +2 -0
- data/sig/chainable.rbs +2 -1
- data/sig/connection/http1.rbs +6 -1
- data/sig/connection/http2.rbs +6 -2
- data/sig/headers.rbs +2 -2
- data/sig/options.rbs +16 -22
- data/sig/plugins/aws_sdk_authentication.rbs +2 -0
- data/sig/plugins/aws_sigv4.rbs +0 -1
- data/sig/plugins/basic_authentication.rbs +2 -0
- data/sig/plugins/compression.rbs +7 -5
- data/sig/plugins/compression/brotli.rbs +1 -1
- data/sig/plugins/compression/deflate.rbs +1 -1
- data/sig/plugins/compression/gzip.rbs +1 -1
- data/sig/plugins/cookies.rbs +0 -1
- data/sig/plugins/digest_authentication.rbs +0 -1
- data/sig/plugins/expect.rbs +0 -2
- data/sig/plugins/follow_redirects.rbs +0 -2
- data/sig/plugins/h2c.rbs +5 -10
- data/sig/plugins/persistent.rbs +0 -1
- data/sig/plugins/proxy.rbs +0 -1
- data/sig/plugins/retries.rbs +0 -4
- data/sig/plugins/stream.rbs +17 -16
- data/sig/plugins/upgrade.rbs +23 -0
- data/sig/request.rbs +7 -2
- data/sig/response.rbs +4 -1
- data/sig/session.rbs +4 -0
- metadata +21 -7
- data/lib/httpx/timeout.rb +0 -67
- data/sig/timeout.rbs +0 -29
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin helps negotiating a new protocol from an HTTP/1.1 connection, via the
|
7
|
+
# Upgrade header.
|
8
|
+
#
|
9
|
+
# https://gitlab.com/honeyryderchuck/httpx/wikis/Upgrade
|
10
|
+
#
|
11
|
+
module Upgrade
|
12
|
+
class << self
|
13
|
+
def configure(klass)
|
14
|
+
klass.plugin(:"upgrade/h2")
|
15
|
+
end
|
16
|
+
|
17
|
+
def extra_options(options)
|
18
|
+
upgrade_handlers = Module.new do
|
19
|
+
extend Registry
|
20
|
+
end
|
21
|
+
|
22
|
+
Class.new(options.class) do
|
23
|
+
def_option(:upgrade_handlers, <<-OUT)
|
24
|
+
raise Error, ":upgrade_handlers must be a registry" unless value.respond_to?(:registry)
|
25
|
+
|
26
|
+
value
|
27
|
+
OUT
|
28
|
+
end.new(options).merge(upgrade_handlers: upgrade_handlers)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
def fetch_response(request, connections, options)
|
34
|
+
response = super
|
35
|
+
|
36
|
+
if response
|
37
|
+
return response unless response.respond_to?(:headers) && response.headers.key?("upgrade")
|
38
|
+
|
39
|
+
upgrade_protocol = response.headers["upgrade"].split(/ *, */).first
|
40
|
+
|
41
|
+
return response unless upgrade_protocol && options.upgrade_handlers.registry.key?(upgrade_protocol)
|
42
|
+
|
43
|
+
protocol_handler = options.upgrade_handlers.registry(upgrade_protocol)
|
44
|
+
|
45
|
+
return response unless protocol_handler
|
46
|
+
|
47
|
+
log { "upgrading to #{upgrade_protocol}..." }
|
48
|
+
connection = find_connection(request, connections, options)
|
49
|
+
connections << connection unless connections.include?(connection)
|
50
|
+
|
51
|
+
# do not upgrade already upgraded connections
|
52
|
+
return if connection.upgrade_protocol == upgrade_protocol
|
53
|
+
|
54
|
+
protocol_handler.call(connection, request, response)
|
55
|
+
|
56
|
+
# keep in the loop if the server is switching, unless
|
57
|
+
# the connection has been hijacked, in which case you want
|
58
|
+
# to terminante immediately
|
59
|
+
return if response.status == 101 && !connection.hijacked
|
60
|
+
end
|
61
|
+
|
62
|
+
response
|
63
|
+
end
|
64
|
+
|
65
|
+
def close(*args)
|
66
|
+
return super if args.empty?
|
67
|
+
|
68
|
+
connections, = args
|
69
|
+
|
70
|
+
pool.close(connections.reject(&:hijacked))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module ConnectionMethods
|
75
|
+
attr_reader :upgrade_protocol, :hijacked
|
76
|
+
|
77
|
+
def hijack_io
|
78
|
+
@hijacked = true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
register_plugin(:upgrade, Upgrade)
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds support for upgrading an HTTP/1.1 connection to HTTP/2
|
7
|
+
# via an Upgrade: h2 response declaration
|
8
|
+
#
|
9
|
+
# https://gitlab.com/honeyryderchuck/httpx/wikis/Upgrade#h2
|
10
|
+
#
|
11
|
+
module H2
|
12
|
+
class << self
|
13
|
+
def configure(klass)
|
14
|
+
klass.default_options.upgrade_handlers.register "h2", self
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(connection, _request, _response)
|
18
|
+
connection.upgrade_to_h2
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ConnectionMethods
|
23
|
+
using URIExtensions
|
24
|
+
|
25
|
+
def upgrade_to_h2
|
26
|
+
prev_parser = @parser
|
27
|
+
|
28
|
+
if prev_parser
|
29
|
+
prev_parser.reset
|
30
|
+
@inflight -= prev_parser.requests.size
|
31
|
+
end
|
32
|
+
|
33
|
+
@parser = Connection::HTTP2.new(@write_buffer, @options)
|
34
|
+
set_parser_callbacks(@parser)
|
35
|
+
@upgrade_protocol = :h2
|
36
|
+
|
37
|
+
# what's happening here:
|
38
|
+
# a deviation from the state machine is done to perform the actions when a
|
39
|
+
# connection is closed, without transitioning, so the connection is kept in the pool.
|
40
|
+
# the state is reset to initial, so that the socket reconnect works out of the box,
|
41
|
+
# while the parser is already here.
|
42
|
+
purge_after_closed
|
43
|
+
transition(:idle)
|
44
|
+
|
45
|
+
prev_parser.requests.each do |req|
|
46
|
+
req.transition(:idle)
|
47
|
+
send(req)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
register_plugin(:"upgrade/h2", H2)
|
53
|
+
end
|
54
|
+
end
|
data/lib/httpx/pool.rb
CHANGED
@@ -64,11 +64,6 @@ module HTTPX
|
|
64
64
|
connection.on(:open) do
|
65
65
|
@connected_connections += 1
|
66
66
|
end
|
67
|
-
connection.on(:unreachable) do
|
68
|
-
resolver = find_resolver_for(connection)
|
69
|
-
resolver.uncache(connection) if resolver
|
70
|
-
resolve_connection(connection)
|
71
|
-
end
|
72
67
|
end
|
73
68
|
|
74
69
|
# opens a connection to the IP reachable through +uri+.
|
@@ -85,6 +80,20 @@ module HTTPX
|
|
85
80
|
|
86
81
|
def resolve_connection(connection)
|
87
82
|
@connections << connection unless @connections.include?(connection)
|
83
|
+
|
84
|
+
if connection.addresses || connection.state == :open
|
85
|
+
#
|
86
|
+
# there are two cases in which we want to activate initialization of
|
87
|
+
# connection immediately:
|
88
|
+
#
|
89
|
+
# 1. when the connection already has addresses, i.e. it doesn't need to
|
90
|
+
# resolve a name (not the same as name being an IP, yet)
|
91
|
+
# 2. when the connection is initialized with an external already open IO.
|
92
|
+
#
|
93
|
+
on_resolver_connection(connection)
|
94
|
+
return
|
95
|
+
end
|
96
|
+
|
88
97
|
resolver = find_resolver_for(connection)
|
89
98
|
resolver << connection
|
90
99
|
return if resolver.empty?
|
data/lib/httpx/request.rb
CHANGED
@@ -35,14 +35,22 @@ module HTTPX
|
|
35
35
|
|
36
36
|
attr_reader :verb, :uri, :headers, :body, :state, :options, :response
|
37
37
|
|
38
|
+
# Exception raised during enumerable body writes
|
39
|
+
attr_reader :drain_error
|
40
|
+
|
38
41
|
def_delegator :@body, :empty?
|
39
42
|
|
40
43
|
def_delegator :@body, :chunk!
|
41
44
|
|
42
45
|
def initialize(verb, uri, options = {})
|
43
46
|
@verb = verb.to_s.downcase.to_sym
|
44
|
-
@uri = Utils.uri(uri)
|
45
47
|
@options = Options.new(options)
|
48
|
+
@uri = Utils.uri(uri)
|
49
|
+
if @uri.relative?
|
50
|
+
raise(Error, "invalid URI: #{@uri}") unless @options.origin
|
51
|
+
|
52
|
+
@uri = @options.origin.merge(@uri)
|
53
|
+
end
|
46
54
|
|
47
55
|
raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
48
56
|
|
@@ -54,6 +62,14 @@ module HTTPX
|
|
54
62
|
@state = :idle
|
55
63
|
end
|
56
64
|
|
65
|
+
def trailers?
|
66
|
+
defined?(@trailers)
|
67
|
+
end
|
68
|
+
|
69
|
+
def trailers
|
70
|
+
@trailers ||= @options.headers_class.new
|
71
|
+
end
|
72
|
+
|
57
73
|
def interests
|
58
74
|
return :r if @state == :done || @state == :expect
|
59
75
|
|
@@ -124,6 +140,9 @@ module HTTPX
|
|
124
140
|
chunk.dup
|
125
141
|
rescue StopIteration
|
126
142
|
nil
|
143
|
+
rescue StandardError => e
|
144
|
+
@drain_error = e
|
145
|
+
nil
|
127
146
|
end
|
128
147
|
|
129
148
|
# :nocov:
|
@@ -200,7 +219,9 @@ module HTTPX
|
|
200
219
|
end
|
201
220
|
|
202
221
|
def unbounded_body?
|
203
|
-
|
222
|
+
return @unbounded_body if defined?(@unbounded_body)
|
223
|
+
|
224
|
+
@unbounded_body = (chunked? || @body.bytesize == Float::INFINITY)
|
204
225
|
end
|
205
226
|
|
206
227
|
def chunked?
|
@@ -253,6 +274,8 @@ module HTTPX
|
|
253
274
|
nextstate = :expect
|
254
275
|
end
|
255
276
|
end
|
277
|
+
when :trailers
|
278
|
+
return unless @state == :body
|
256
279
|
when :done
|
257
280
|
return if @state == :expect
|
258
281
|
end
|
@@ -101,7 +101,7 @@ module HTTPX
|
|
101
101
|
transition(:open)
|
102
102
|
end
|
103
103
|
|
104
|
-
|
104
|
+
calculate_interests
|
105
105
|
end
|
106
106
|
|
107
107
|
def <<(connection)
|
@@ -127,10 +127,14 @@ module HTTPX
|
|
127
127
|
|
128
128
|
private
|
129
129
|
|
130
|
+
def calculate_interests
|
131
|
+
!@write_buffer.empty? || @queries.empty? ? :w : :r
|
132
|
+
end
|
133
|
+
|
130
134
|
def consume
|
131
|
-
dread
|
135
|
+
dread if calculate_interests == :r
|
132
136
|
do_retry
|
133
|
-
dwrite
|
137
|
+
dwrite if calculate_interests == :w
|
134
138
|
end
|
135
139
|
|
136
140
|
def do_retry
|
data/lib/httpx/response.rb
CHANGED
@@ -27,8 +27,7 @@ module HTTPX
|
|
27
27
|
@version = version
|
28
28
|
@status = Integer(status)
|
29
29
|
@headers = @options.headers_class.new(headers)
|
30
|
-
@body = @options.response_body_class.new(self,
|
31
|
-
window_size: @options.window_size)
|
30
|
+
@body = @options.response_body_class.new(self, @options)
|
32
31
|
end
|
33
32
|
|
34
33
|
def merge_headers(h)
|
@@ -83,17 +82,22 @@ module HTTPX
|
|
83
82
|
end
|
84
83
|
|
85
84
|
class Body
|
86
|
-
def initialize(response,
|
85
|
+
def initialize(response, options)
|
87
86
|
@response = response
|
88
87
|
@headers = response.headers
|
89
|
-
@
|
90
|
-
@
|
88
|
+
@options = options
|
89
|
+
@threshold_size = options.body_threshold_size
|
90
|
+
@window_size = options.window_size
|
91
91
|
@encoding = response.content_type.charset || Encoding::BINARY
|
92
92
|
@length = 0
|
93
93
|
@buffer = nil
|
94
94
|
@state = :idle
|
95
95
|
end
|
96
96
|
|
97
|
+
def closed?
|
98
|
+
@state == :closed
|
99
|
+
end
|
100
|
+
|
97
101
|
def write(chunk)
|
98
102
|
return if @state == :closed
|
99
103
|
|
data/lib/httpx/session.rb
CHANGED
@@ -77,7 +77,7 @@ module HTTPX
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def set_connection_callbacks(connection, connections, options)
|
80
|
-
connection.
|
80
|
+
connection.only(:misdirected) do |misdirected_request|
|
81
81
|
other_connection = connection.create_idle(ssl: { alpn_protocols: %w[http/1.1] })
|
82
82
|
other_connection.merge(connection)
|
83
83
|
catch(:coalesced) do
|
@@ -88,11 +88,11 @@ module HTTPX
|
|
88
88
|
misdirected_request.transition(:idle)
|
89
89
|
other_connection.send(misdirected_request)
|
90
90
|
end
|
91
|
-
connection.
|
91
|
+
connection.only(:altsvc) do |alt_origin, origin, alt_params|
|
92
92
|
other_connection = build_altsvc_connection(connection, connections, alt_origin, origin, alt_params, options)
|
93
93
|
connections << other_connection if other_connection
|
94
94
|
end
|
95
|
-
connection.
|
95
|
+
connection.only(:exhausted) do
|
96
96
|
other_connection = connection.create_idle
|
97
97
|
other_connection.merge(connection)
|
98
98
|
catch(:coalesced) do
|
@@ -175,12 +175,18 @@ module HTTPX
|
|
175
175
|
end
|
176
176
|
|
177
177
|
def send_requests(*requests, options)
|
178
|
-
connections = []
|
179
178
|
request_options = @options.merge(options)
|
180
179
|
|
180
|
+
connections = _send_requests(requests, request_options)
|
181
|
+
receive_requests(requests, connections, request_options)
|
182
|
+
end
|
183
|
+
|
184
|
+
def _send_requests(requests, options)
|
185
|
+
connections = []
|
186
|
+
|
181
187
|
requests.each do |request|
|
182
188
|
error = catch(:resolve_error) do
|
183
|
-
connection = find_connection(request, connections,
|
189
|
+
connection = find_connection(request, connections, options)
|
184
190
|
connection.send(request)
|
185
191
|
end
|
186
192
|
next unless error.is_a?(ResolveError)
|
@@ -188,13 +194,17 @@ module HTTPX
|
|
188
194
|
request.emit(:response, ErrorResponse.new(request, error, options))
|
189
195
|
end
|
190
196
|
|
197
|
+
connections
|
198
|
+
end
|
199
|
+
|
200
|
+
def receive_requests(requests, connections, options)
|
191
201
|
responses = []
|
192
202
|
|
193
203
|
begin
|
194
204
|
# guarantee ordered responses
|
195
205
|
loop do
|
196
206
|
request = requests.first
|
197
|
-
pool.next_tick until (response = fetch_response(request, connections,
|
207
|
+
pool.next_tick until (response = fetch_response(request, connections, options))
|
198
208
|
|
199
209
|
responses << response
|
200
210
|
requests.shift
|
@@ -208,7 +218,7 @@ module HTTPX
|
|
208
218
|
# opportunity to traverse the requests, hence we're returning only a fraction of the errors
|
209
219
|
# we were supposed to. This effectively fetches the existing responses and return them.
|
210
220
|
while (request = requests.shift)
|
211
|
-
responses << fetch_response(request, connections,
|
221
|
+
responses << fetch_response(request, connections, options)
|
212
222
|
end
|
213
223
|
break
|
214
224
|
end
|
data/lib/httpx/version.rb
CHANGED
data/sig/callbacks.rbs
CHANGED
@@ -6,8 +6,10 @@ module HTTPX
|
|
6
6
|
module Callbacks
|
7
7
|
def on: (Symbol) { (*untyped) -> void } -> void
|
8
8
|
def once: (Symbol) { (*untyped) -> void } -> void
|
9
|
+
def only: (Symbol) { (*untyped) -> void } -> void
|
9
10
|
def emit: (Symbol, *untyped) -> void
|
10
11
|
|
12
|
+
def callbacks_for?: (Symbol) -> bool
|
11
13
|
def callbacks: () -> Hash[Symbol, Array[_Callable]]
|
12
14
|
| (Symbol) -> Array[_Callable]
|
13
15
|
end
|
data/sig/chainable.rbs
CHANGED
@@ -18,7 +18,8 @@ module HTTPX
|
|
18
18
|
| (:cookies) -> Plugins::sessionCookies
|
19
19
|
| (:expect) -> Session
|
20
20
|
| (:follow_redirects) -> Plugins::sessionFollowRedirects
|
21
|
-
| (:
|
21
|
+
| (:upgrade) -> Session
|
22
|
+
| (:h2c) -> Session
|
22
23
|
| (:multipart) -> Session
|
23
24
|
| (:persistent) -> Plugins::sessionPersistent
|
24
25
|
| (:proxy) -> Plugins::sessionProxy
|
data/sig/connection/http1.rbs
CHANGED
@@ -4,6 +4,7 @@ module HTTPX
|
|
4
4
|
include Loggable
|
5
5
|
|
6
6
|
attr_reader pending: Array[Request]
|
7
|
+
attr_reader requests: Array[Request]
|
7
8
|
|
8
9
|
@options: Options
|
9
10
|
@max_concurrent_requests: Integer
|
@@ -51,7 +52,7 @@ module HTTPX
|
|
51
52
|
|
52
53
|
def disable_pipelining: () -> void
|
53
54
|
|
54
|
-
def set_protocol_headers: (Request) ->
|
55
|
+
def set_protocol_headers: (Request) -> _Each[headers_key, String]
|
55
56
|
|
56
57
|
def headline_uri: (Request) -> String
|
57
58
|
|
@@ -59,6 +60,10 @@ module HTTPX
|
|
59
60
|
|
60
61
|
def join_headers: (Request request) -> void
|
61
62
|
|
63
|
+
def join_trailers: (Request request) -> void
|
64
|
+
|
65
|
+
def join_headers2: (_Each[headers_key, String] headers) -> void
|
66
|
+
|
62
67
|
def join_body: (Request request) -> void
|
63
68
|
|
64
69
|
def capitalized: (String field) -> String
|
data/sig/connection/http2.rbs
CHANGED
@@ -43,7 +43,7 @@ module HTTPX
|
|
43
43
|
|
44
44
|
def headline_uri: (Request) -> String
|
45
45
|
|
46
|
-
def set_protocol_headers: (Request) ->
|
46
|
+
def set_protocol_headers: (Request) -> _Each[headers_key, String]
|
47
47
|
|
48
48
|
def handle: (Request request, HTTP2Next::Stream stream) -> void
|
49
49
|
|
@@ -53,13 +53,17 @@ module HTTPX
|
|
53
53
|
|
54
54
|
def join_headers: (HTTP2Next::Stream stream, Request request) -> void
|
55
55
|
|
56
|
+
def join_trailers: (HTTP2Next::Stream stream, Request request) -> void
|
57
|
+
|
56
58
|
def join_body: (HTTP2Next::Stream stream, Request request) -> void
|
57
59
|
|
58
60
|
def on_stream_headers: (HTTP2Next::Stream stream, Request request, Array[[String, String]] headers) -> void
|
59
61
|
|
62
|
+
def on_stream_trailers: (HTTP2Next::Stream stream, Request request, Array[[String, String]] headers) -> void
|
63
|
+
|
60
64
|
def on_stream_data: (HTTP2Next::Stream stream, Request request, string data) -> void
|
61
65
|
|
62
|
-
def on_stream_close: (HTTP2Next::Stream stream, Request request, Symbol? error) -> void
|
66
|
+
def on_stream_close: (HTTP2Next::Stream stream, Request request, (Symbol | StandardError)? error) -> void
|
63
67
|
|
64
68
|
def on_frame: (string bytes) -> void
|
65
69
|
|