httpx 0.16.1 → 0.17.0
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_17_0.md +49 -0
- data/lib/httpx/adapters/webmock.rb +2 -2
- data/lib/httpx/chainable.rb +1 -1
- data/lib/httpx/connection/http1.rb +15 -9
- data/lib/httpx/connection/http2.rb +13 -10
- data/lib/httpx/connection.rb +4 -5
- data/lib/httpx/headers.rb +1 -1
- data/lib/httpx/options.rb +28 -6
- data/lib/httpx/parser/http1.rb +10 -6
- data/lib/httpx/plugins/digest_authentication.rb +4 -4
- data/lib/httpx/plugins/h2c.rb +7 -3
- data/lib/httpx/plugins/multipart/decoder.rb +187 -0
- data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
- data/lib/httpx/plugins/multipart/part.rb +2 -2
- data/lib/httpx/plugins/multipart.rb +14 -0
- data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
- data/lib/httpx/plugins/proxy/ssh.rb +11 -4
- data/lib/httpx/plugins/proxy.rb +6 -4
- data/lib/httpx/plugins/stream.rb +2 -3
- data/lib/httpx/registry.rb +1 -1
- data/lib/httpx/request.rb +6 -7
- data/lib/httpx/resolver/resolver_mixin.rb +2 -1
- data/lib/httpx/response.rb +37 -30
- data/lib/httpx/selector.rb +4 -2
- data/lib/httpx/session.rb +15 -13
- data/lib/httpx/transcoder/form.rb +20 -0
- data/lib/httpx/transcoder/json.rb +12 -0
- data/lib/httpx/transcoder.rb +62 -1
- data/lib/httpx/utils.rb +2 -2
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +2 -2
- data/sig/chainable.rbs +6 -1
- data/sig/connection/http1.rbs +10 -4
- data/sig/connection/http2.rbs +16 -5
- data/sig/connection.rbs +4 -4
- data/sig/headers.rbs +19 -18
- data/sig/options.rbs +13 -5
- data/sig/parser/http1.rbs +3 -3
- data/sig/plugins/aws_sigv4.rbs +12 -3
- data/sig/plugins/basic_authentication.rbs +1 -1
- data/sig/plugins/multipart.rbs +64 -8
- data/sig/plugins/proxy.rbs +6 -6
- data/sig/request.rbs +11 -8
- data/sig/resolver/native.rbs +4 -2
- data/sig/resolver/resolver_mixin.rbs +1 -1
- data/sig/resolver/system.rbs +1 -1
- data/sig/response.rbs +8 -2
- data/sig/selector.rbs +8 -6
- data/sig/session.rbs +8 -14
- data/sig/transcoder/form.rbs +1 -0
- data/sig/transcoder/json.rbs +1 -0
- data/sig/transcoder.rbs +5 -4
- metadata +5 -2
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
require "delegate"
|
5
|
+
|
6
|
+
module HTTPX::Plugins
|
7
|
+
module Multipart
|
8
|
+
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
9
|
+
|
10
|
+
CRLF = "\r\n"
|
11
|
+
|
12
|
+
class FilePart < SimpleDelegator
|
13
|
+
attr_reader :original_filename, :content_type
|
14
|
+
|
15
|
+
def initialize(filename, content_type)
|
16
|
+
@original_filename = filename
|
17
|
+
@content_type = content_type
|
18
|
+
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
19
|
+
super(@file)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
24
|
+
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
25
|
+
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i.freeze
|
26
|
+
BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i.freeze
|
27
|
+
BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i.freeze
|
28
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
29
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
30
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
31
|
+
# Updated definitions from RFC 2231
|
32
|
+
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}.freeze
|
33
|
+
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/.freeze
|
34
|
+
SECTION = /\*[0-9]+/.freeze
|
35
|
+
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/.freeze
|
36
|
+
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/.freeze
|
37
|
+
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/.freeze
|
38
|
+
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/.freeze
|
39
|
+
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/.freeze
|
40
|
+
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/.freeze
|
41
|
+
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/.freeze
|
42
|
+
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/.freeze
|
43
|
+
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/.freeze
|
44
|
+
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/.freeze
|
45
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i.freeze
|
46
|
+
|
47
|
+
class Decoder
|
48
|
+
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
49
|
+
WINDOW_SIZE = 2 << 14
|
50
|
+
|
51
|
+
def initialize(response)
|
52
|
+
@boundary = begin
|
53
|
+
m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
|
54
|
+
raise Error, "no boundary declared in content-type header" unless m
|
55
|
+
|
56
|
+
m.strip
|
57
|
+
end
|
58
|
+
@buffer = "".b
|
59
|
+
@parts = {}
|
60
|
+
@intermediate_boundary = "--#{@boundary}"
|
61
|
+
@state = :idle
|
62
|
+
end
|
63
|
+
|
64
|
+
def call(response, _)
|
65
|
+
response.body.each do |chunk|
|
66
|
+
@buffer << chunk
|
67
|
+
|
68
|
+
parse
|
69
|
+
end
|
70
|
+
|
71
|
+
raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
|
72
|
+
|
73
|
+
@parts
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def parse
|
79
|
+
case @state
|
80
|
+
when :idle
|
81
|
+
raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
|
82
|
+
|
83
|
+
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
|
84
|
+
|
85
|
+
@state = :part_header
|
86
|
+
when :part_header
|
87
|
+
idx = @buffer.index("#{CRLF}#{CRLF}")
|
88
|
+
|
89
|
+
# raise Error, "couldn't parse part headers" unless idx
|
90
|
+
return unless idx
|
91
|
+
|
92
|
+
head = @buffer.byteslice(0..idx + 4 - 1)
|
93
|
+
|
94
|
+
@buffer = @buffer.byteslice(head.bytesize..-1)
|
95
|
+
|
96
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
97
|
+
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
98
|
+
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
99
|
+
name.gsub!(/\\(.)/, "\\1")
|
100
|
+
name
|
101
|
+
else
|
102
|
+
name = head[MULTIPART_CONTENT_ID, 1]
|
103
|
+
end
|
104
|
+
|
105
|
+
filename = get_filename(head)
|
106
|
+
|
107
|
+
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
108
|
+
|
109
|
+
@current = name
|
110
|
+
|
111
|
+
@parts[name] = if filename
|
112
|
+
FilePart.new(filename, content_type)
|
113
|
+
else
|
114
|
+
"".b
|
115
|
+
end
|
116
|
+
|
117
|
+
@state = :part_body
|
118
|
+
when :part_body
|
119
|
+
part = @parts[@current]
|
120
|
+
|
121
|
+
body_separator = if part.is_a?(FilePart)
|
122
|
+
"#{CRLF}#{CRLF}"
|
123
|
+
else
|
124
|
+
CRLF
|
125
|
+
end
|
126
|
+
idx = @buffer.index(body_separator)
|
127
|
+
|
128
|
+
if idx
|
129
|
+
payload = @buffer.byteslice(0..idx - 1)
|
130
|
+
@buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
|
131
|
+
part << payload
|
132
|
+
part.rewind if part.respond_to?(:rewind)
|
133
|
+
@state = :parse_boundary
|
134
|
+
else
|
135
|
+
part << @buffer
|
136
|
+
@buffer.clear
|
137
|
+
end
|
138
|
+
when :parse_boundary
|
139
|
+
raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
|
140
|
+
|
141
|
+
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
|
142
|
+
|
143
|
+
if @buffer == "--"
|
144
|
+
@buffer.clear
|
145
|
+
@state = :done
|
146
|
+
return
|
147
|
+
elsif @buffer.start_with?(CRLF)
|
148
|
+
@buffer = @buffer.byteslice(2..-1)
|
149
|
+
@state = :part_header
|
150
|
+
else
|
151
|
+
return
|
152
|
+
end
|
153
|
+
when :done
|
154
|
+
raise Error, "parsing should have been over by now"
|
155
|
+
end until @buffer.empty?
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_filename(head)
|
159
|
+
filename = nil
|
160
|
+
case head
|
161
|
+
when RFC2183
|
162
|
+
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
163
|
+
|
164
|
+
if (filename = params["filename"])
|
165
|
+
filename = Regexp.last_match(1) if filename =~ /^"(.*)"$/
|
166
|
+
elsif (filename = params["filename*"])
|
167
|
+
encoding, _, filename = filename.split("'", 3)
|
168
|
+
end
|
169
|
+
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
170
|
+
filename = Regexp.last_match(1)
|
171
|
+
end
|
172
|
+
|
173
|
+
return unless filename
|
174
|
+
|
175
|
+
filename = URI::DEFAULT_PARSER.unescape(filename) if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
176
|
+
|
177
|
+
filename.scrub!
|
178
|
+
|
179
|
+
filename = filename.gsub(/\\(.)/, '\1') unless /\\[^\\"]/.match?(filename)
|
180
|
+
|
181
|
+
filename.force_encoding ::Encoding.find(encoding) if encoding
|
182
|
+
|
183
|
+
filename
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -17,7 +17,7 @@ module HTTPX
|
|
17
17
|
|
18
18
|
elsif defined?(MimeMagic)
|
19
19
|
|
20
|
-
def call(file,
|
20
|
+
def call(file, _)
|
21
21
|
mime = MimeMagic.by_magic(file)
|
22
22
|
mime.type if mime
|
23
23
|
end
|
@@ -25,7 +25,7 @@ module HTTPX
|
|
25
25
|
elsif system("which file", out: File::NULL)
|
26
26
|
require "open3"
|
27
27
|
|
28
|
-
def call(file,
|
28
|
+
def call(file, _)
|
29
29
|
return if file.eof? # file command returns "application/x-empty" for empty files
|
30
30
|
|
31
31
|
Open3.popen3(*%w[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
|
@@ -56,7 +56,7 @@ module HTTPX
|
|
56
56
|
|
57
57
|
else
|
58
58
|
|
59
|
-
def call(
|
59
|
+
def call(_, _); end
|
60
60
|
|
61
61
|
end
|
62
62
|
end
|
@@ -8,7 +8,7 @@ module HTTPX
|
|
8
8
|
def call(value)
|
9
9
|
# take out specialized objects of the way
|
10
10
|
if value.respond_to?(:filename) && value.respond_to?(:content_type) && value.respond_to?(:read)
|
11
|
-
return
|
11
|
+
return value, value.content_type, value.filename
|
12
12
|
end
|
13
13
|
|
14
14
|
content_type = filename = nil
|
@@ -19,7 +19,7 @@ module HTTPX
|
|
19
19
|
value = value[:body]
|
20
20
|
end
|
21
21
|
|
22
|
-
value = value.open(
|
22
|
+
value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
|
23
23
|
|
24
24
|
if value.is_a?(File)
|
25
25
|
filename ||= File.basename(value.path)
|
@@ -36,6 +36,7 @@ module HTTPX
|
|
36
36
|
end
|
37
37
|
# :nocov:
|
38
38
|
require "httpx/plugins/multipart/encoder"
|
39
|
+
require "httpx/plugins/multipart/decoder"
|
39
40
|
require "httpx/plugins/multipart/part"
|
40
41
|
require "httpx/plugins/multipart/mime_type_detector"
|
41
42
|
end
|
@@ -56,6 +57,19 @@ module HTTPX
|
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
60
|
+
def decode(response)
|
61
|
+
content_type = response.content_type.mime_type
|
62
|
+
|
63
|
+
case content_type
|
64
|
+
when "application/x-www-form-urlencoded"
|
65
|
+
Transcoder::Form.decode(response)
|
66
|
+
when "multipart/form-data"
|
67
|
+
Decoder.new(response)
|
68
|
+
else
|
69
|
+
raise Error, "invalid form mime type (#{content_type})"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
59
73
|
def multipart?(data)
|
60
74
|
data.any? do |_, v|
|
61
75
|
MULTIPART_VALUE_COND.call(v) ||
|
@@ -34,13 +34,13 @@ module HTTPX
|
|
34
34
|
|
35
35
|
alias_method :ntlm_auth, :ntlm_authentication
|
36
36
|
|
37
|
-
def send_requests(*requests
|
37
|
+
def send_requests(*requests)
|
38
38
|
requests.flat_map do |request|
|
39
39
|
ntlm = request.options.ntlm
|
40
40
|
|
41
41
|
if ntlm
|
42
42
|
request.headers["authorization"] = "NTLM #{NTLM.negotiate(domain: ntlm.domain).to_base64}"
|
43
|
-
probe_response = wrap { super(request
|
43
|
+
probe_response = wrap { super(request).first }
|
44
44
|
|
45
45
|
if !probe_response.is_a?(ErrorResponse) && probe_response.status == 401 &&
|
46
46
|
probe_response.headers.key?("www-authenticate") &&
|
@@ -52,12 +52,12 @@ module HTTPX
|
|
52
52
|
request.transition(:idle)
|
53
53
|
|
54
54
|
request.headers["authorization"] = "NTLM #{ntlm_challenge}"
|
55
|
-
super(request
|
55
|
+
super(request)
|
56
56
|
else
|
57
57
|
probe_response
|
58
58
|
end
|
59
59
|
else
|
60
|
-
super(request
|
60
|
+
super(request)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
@@ -19,10 +19,14 @@ module HTTPX
|
|
19
19
|
end
|
20
20
|
|
21
21
|
module InstanceMethods
|
22
|
-
|
22
|
+
def request(*args, **options)
|
23
|
+
raise ArgumentError, "must perform at least one request" if args.empty?
|
24
|
+
|
25
|
+
requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
|
26
|
+
|
27
|
+
request = requests.first or return super
|
23
28
|
|
24
|
-
|
25
|
-
request_options = @options.merge(options)
|
29
|
+
request_options = request.options
|
26
30
|
|
27
31
|
return super unless request_options.proxy
|
28
32
|
|
@@ -37,18 +41,21 @@ module HTTPX
|
|
37
41
|
if request_options.debug
|
38
42
|
ssh_options[:verbose] = request_options.debug_level == 2 ? :debug : :info
|
39
43
|
end
|
44
|
+
|
40
45
|
request_uri = URI(requests.first.uri)
|
41
46
|
@_gateway = Net::SSH::Gateway.new(ssh_uri.host, ssh_username, ssh_options)
|
42
47
|
begin
|
43
48
|
@_gateway.open(request_uri.host, request_uri.port) do |local_port|
|
44
49
|
io = build_gateway_socket(local_port, request_uri, request_options)
|
45
|
-
super(*
|
50
|
+
super(*args, **options.merge(io: io))
|
46
51
|
end
|
47
52
|
ensure
|
48
53
|
@_gateway.shutdown!
|
49
54
|
end
|
50
55
|
end
|
51
56
|
|
57
|
+
private
|
58
|
+
|
52
59
|
def build_gateway_socket(port, request_uri, options)
|
53
60
|
case request_uri.scheme
|
54
61
|
when "https"
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -5,7 +5,8 @@ require "ipaddr"
|
|
5
5
|
require "forwardable"
|
6
6
|
|
7
7
|
module HTTPX
|
8
|
-
HTTPProxyError
|
8
|
+
class HTTPProxyError < Error; end
|
9
|
+
|
9
10
|
module Plugins
|
10
11
|
#
|
11
12
|
# This plugin adds support for proxies. It ships with support for:
|
@@ -136,10 +137,11 @@ module HTTPX
|
|
136
137
|
def __proxy_error?(response)
|
137
138
|
error = response.error
|
138
139
|
case error
|
139
|
-
when
|
140
|
+
when NativeResolveError
|
140
141
|
# failed resolving proxy domain
|
141
|
-
|
142
|
-
|
142
|
+
error.connection.origin.to_s == @_proxy_uris.first
|
143
|
+
when ResolveError
|
144
|
+
error.message.end_with?(@_proxy_uris.first)
|
143
145
|
when *PROXY_ERRORS
|
144
146
|
# timeout errors connecting to proxy
|
145
147
|
true
|
data/lib/httpx/plugins/stream.rb
CHANGED
@@ -6,7 +6,6 @@ module HTTPX
|
|
6
6
|
@request = request
|
7
7
|
@session = session
|
8
8
|
@connections = connections
|
9
|
-
@options = @request.options
|
10
9
|
end
|
11
10
|
|
12
11
|
def each(&block)
|
@@ -72,7 +71,7 @@ module HTTPX
|
|
72
71
|
private
|
73
72
|
|
74
73
|
def response
|
75
|
-
@session.__send__(:receive_requests, [@request], @connections
|
74
|
+
@session.__send__(:receive_requests, [@request], @connections) until @request.response
|
76
75
|
|
77
76
|
@request.response
|
78
77
|
end
|
@@ -106,7 +105,7 @@ module HTTPX
|
|
106
105
|
|
107
106
|
request = requests.first
|
108
107
|
|
109
|
-
connections = _send_requests(requests
|
108
|
+
connections = _send_requests(requests)
|
110
109
|
|
111
110
|
StreamResponse.new(request, self, connections)
|
112
111
|
end
|
data/lib/httpx/registry.rb
CHANGED
data/lib/httpx/request.rb
CHANGED
@@ -41,16 +41,15 @@ module HTTPX
|
|
41
41
|
|
42
42
|
def_delegator :@body, :empty?
|
43
43
|
|
44
|
-
def_delegator :@body, :chunk!
|
45
|
-
|
46
44
|
def initialize(verb, uri, options = {})
|
47
45
|
@verb = verb.to_s.downcase.to_sym
|
48
46
|
@options = Options.new(options)
|
49
47
|
@uri = Utils.to_uri(uri)
|
50
48
|
if @uri.relative?
|
51
|
-
|
49
|
+
origin = @options.origin
|
50
|
+
raise(Error, "invalid URI: #{@uri}") unless origin
|
52
51
|
|
53
|
-
@uri =
|
52
|
+
@uri = origin.merge(@uri)
|
54
53
|
end
|
55
54
|
|
56
55
|
raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
@@ -98,7 +97,7 @@ module HTTPX
|
|
98
97
|
def response=(response)
|
99
98
|
return unless response
|
100
99
|
|
101
|
-
if response.status == 100
|
100
|
+
if response.is_a?(Response) && response.status == 100
|
102
101
|
@informational_status = response.status
|
103
102
|
return
|
104
103
|
end
|
@@ -158,7 +157,7 @@ module HTTPX
|
|
158
157
|
|
159
158
|
class Body < SimpleDelegator
|
160
159
|
class << self
|
161
|
-
def new(
|
160
|
+
def new(_, options)
|
162
161
|
return options.body if options.body.is_a?(self)
|
163
162
|
|
164
163
|
super
|
@@ -223,7 +222,7 @@ module HTTPX
|
|
223
222
|
def unbounded_body?
|
224
223
|
return @unbounded_body if defined?(@unbounded_body)
|
225
224
|
|
226
|
-
@unbounded_body = (chunked? || @body.bytesize == Float::INFINITY)
|
225
|
+
@unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
|
227
226
|
end
|
228
227
|
|
229
228
|
def chunked?
|
@@ -9,7 +9,7 @@ module HTTPX
|
|
9
9
|
include Callbacks
|
10
10
|
include Loggable
|
11
11
|
|
12
|
-
CHECK_IF_IP =
|
12
|
+
CHECK_IF_IP = lambda do |name|
|
13
13
|
begin
|
14
14
|
IPAddr.new(name)
|
15
15
|
true
|
@@ -55,6 +55,7 @@ module HTTPX
|
|
55
55
|
return if ips.empty?
|
56
56
|
|
57
57
|
ips.map { |ip| IPAddr.new(ip) }
|
58
|
+
rescue IOError
|
58
59
|
end
|
59
60
|
|
60
61
|
def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
|