restify 2.1.0 → 3.0.0.rc2

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.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +30 -4
  4. data/lib/restify/adapter/base.rb +10 -1
  5. data/lib/restify/adapter/ethon.rb +462 -0
  6. data/lib/restify/adapter/loop_lock.rb +124 -0
  7. data/lib/restify/adapter/telemetry.rb +6 -4
  8. data/lib/restify/adapter/typhoeus.rb +48 -23
  9. data/lib/restify/context.rb +5 -5
  10. data/lib/restify/error.rb +13 -0
  11. data/lib/restify/global.rb +9 -30
  12. data/lib/restify/logging.rb +10 -4
  13. data/lib/restify/processors/base/parsing.rb +13 -1
  14. data/lib/restify/promise.rb +20 -2
  15. data/lib/restify/registry.rb +2 -2
  16. data/lib/restify/relation.rb +8 -8
  17. data/lib/restify/request.rb +2 -2
  18. data/lib/restify/resource.rb +20 -2
  19. data/lib/restify/response.rb +39 -3
  20. data/lib/restify/timeout.rb +11 -5
  21. data/lib/restify/version.rb +3 -3
  22. data/lib/restify/webmock.rb +126 -0
  23. data/lib/restify.rb +5 -1
  24. data/spec/restify/adapter/ethon_spec.rb +263 -0
  25. data/spec/restify/adapter/loop_lock_spec.rb +144 -0
  26. data/spec/restify/adapter/telemetry_spec.rb +61 -0
  27. data/spec/restify/adapter/typhoeus_spec.rb +26 -0
  28. data/spec/restify/cache_spec.rb +1 -3
  29. data/spec/restify/context_spec.rb +55 -0
  30. data/spec/restify/features/protocols_spec.rb +28 -0
  31. data/spec/restify/features/redirects_spec.rb +45 -0
  32. data/spec/restify/features/request_errors_spec.rb +1 -1
  33. data/spec/restify/features/response_bodies_spec.rb +45 -0
  34. data/spec/restify/features/response_encoding_spec.rb +82 -0
  35. data/spec/restify/features/response_errors_spec.rb +22 -1
  36. data/spec/restify/features/webmock_spec.rb +92 -1
  37. data/spec/restify/global_spec.rb +78 -10
  38. data/spec/restify/processors/json_spec.rb +36 -0
  39. data/spec/restify/processors/msgpack_spec.rb +36 -0
  40. data/spec/restify/promise_spec.rb +82 -0
  41. data/spec/restify/relation_spec.rb +2 -6
  42. data/spec/restify/request_spec.rb +26 -0
  43. data/spec/restify/timeout_spec.rb +11 -0
  44. data/spec/spec_helper.rb +10 -6
  45. data/spec/support/stub_server.rb +4 -4
  46. metadata +26 -209
  47. data/vendor/bundle/ruby/4.0.0/bundler/gems/my-rubocop-2b861962124a/LICENSE +0 -21
  48. data/vendor/bundle/ruby/4.0.0/bundler/gems/my-rubocop-2b861962124a/README.md +0 -23
  49. data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/CHANGELOG.md +0 -561
  50. data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/MIT-LICENSE +0 -20
  51. data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/README.rdoc +0 -40
  52. data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/CHANGELOG.md +0 -326
  53. data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/LICENSE.txt +0 -202
  54. data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/README.md +0 -121
  55. data/vendor/bundle/ruby/4.0.0/gems/ast-2.4.3/LICENSE.MIT +0 -20
  56. data/vendor/bundle/ruby/4.0.0/gems/ast-2.4.3/README.YARD.md +0 -12
  57. data/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/README.md +0 -48
  58. data/vendor/bundle/ruby/4.0.0/gems/bigdecimal-4.1.2/LICENSE +0 -56
  59. data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/CHANGELOG.md +0 -615
  60. data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/LICENSE.txt +0 -21
  61. data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/README.md +0 -409
  62. data/vendor/bundle/ruby/4.0.0/gems/connection_pool-3.0.2/LICENSE +0 -20
  63. data/vendor/bundle/ruby/4.0.0/gems/connection_pool-3.0.2/README.md +0 -182
  64. data/vendor/bundle/ruby/4.0.0/gems/crack-1.0.1/LICENSE +0 -20
  65. data/vendor/bundle/ruby/4.0.0/gems/crack-1.0.1/README.md +0 -43
  66. data/vendor/bundle/ruby/4.0.0/gems/diff-lcs-1.6.2/CHANGELOG.md +0 -518
  67. data/vendor/bundle/ruby/4.0.0/gems/diff-lcs-1.6.2/README.md +0 -92
  68. data/vendor/bundle/ruby/4.0.0/gems/docile-1.4.1/LICENSE +0 -21
  69. data/vendor/bundle/ruby/4.0.0/gems/docile-1.4.1/README.md +0 -409
  70. data/vendor/bundle/ruby/4.0.0/gems/drb-2.2.3/LICENSE.txt +0 -22
  71. data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/CHANGELOG.md +0 -417
  72. data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/LICENSE +0 -20
  73. data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/README.md +0 -118
  74. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/CHANGELOG.md +0 -507
  75. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/LICENSE +0 -24
  76. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/LICENSE.SPECS +0 -22
  77. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/README.md +0 -138
  78. data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/LICENSE +0 -19
  79. data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/README.md +0 -323
  80. data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/changelog.md +0 -127
  81. data/vendor/bundle/ruby/4.0.0/gems/hitimes-3.2.0/LICENSE.txt +0 -16
  82. data/vendor/bundle/ruby/4.0.0/gems/hitimes-3.2.0/README.md +0 -187
  83. data/vendor/bundle/ruby/4.0.0/gems/i18n-1.15.2/MIT-LICENSE +0 -20
  84. data/vendor/bundle/ruby/4.0.0/gems/i18n-1.15.2/README.md +0 -139
  85. data/vendor/bundle/ruby/4.0.0/gems/json-2.19.9/README.md +0 -310
  86. data/vendor/bundle/ruby/4.0.0/gems/language_server-protocol-3.17.0.5/LICENSE.txt +0 -21
  87. data/vendor/bundle/ruby/4.0.0/gems/language_server-protocol-3.17.0.5/README.md +0 -88
  88. data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/CHANGELOG.md +0 -15
  89. data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/LICENSE.txt +0 -21
  90. data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/README.md +0 -173
  91. data/vendor/bundle/ruby/4.0.0/gems/little-plugger-1.1.4/README.rdoc +0 -53
  92. data/vendor/bundle/ruby/4.0.0/gems/logger-1.7.0/README.md +0 -104
  93. data/vendor/bundle/ruby/4.0.0/gems/logging-2.4.0/LICENSE +0 -22
  94. data/vendor/bundle/ruby/4.0.0/gems/logging-2.4.0/README.md +0 -140
  95. data/vendor/bundle/ruby/4.0.0/gems/minitest-6.0.6/README.rdoc +0 -763
  96. data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/ChangeLog +0 -383
  97. data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/LICENSE +0 -177
  98. data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/README.md +0 -302
  99. data/vendor/bundle/ruby/4.0.0/gems/multi_json-1.21.1/LICENSE.md +0 -20
  100. data/vendor/bundle/ruby/4.0.0/gems/multi_json-1.21.1/README.md +0 -281
  101. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/ext/libev/LICENSE +0 -37
  102. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/ext/libev/README +0 -59
  103. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/license.md +0 -80
  104. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/readme.md +0 -91
  105. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/CHANGELOG.md +0 -219
  106. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/LICENSE +0 -201
  107. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/README.md +0 -68
  108. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/CHANGELOG.md +0 -110
  109. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/LICENSE +0 -201
  110. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/README.md +0 -62
  111. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/CHANGELOG.md +0 -97
  112. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/LICENSE +0 -201
  113. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/README.md +0 -155
  114. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/CHANGELOG.md +0 -192
  115. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/LICENSE +0 -201
  116. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/README.md +0 -66
  117. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/CHANGELOG.md +0 -31
  118. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/LICENSE +0 -201
  119. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/README.md +0 -40
  120. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/CHANGELOG.md +0 -302
  121. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/LICENSE +0 -201
  122. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/README.md +0 -101
  123. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/CHANGELOG.md +0 -65
  124. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/LICENSE +0 -201
  125. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/README.md +0 -115
  126. data/vendor/bundle/ruby/4.0.0/gems/parallel-2.1.0/MIT-LICENSE.txt +0 -20
  127. data/vendor/bundle/ruby/4.0.0/gems/parser-3.3.11.1/LICENSE.txt +0 -26
  128. data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/CHANGELOG.md +0 -786
  129. data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/LICENSE.md +0 -7
  130. data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/README.md +0 -143
  131. data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/CHANGELOG.md +0 -649
  132. data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/LICENSE.txt +0 -22
  133. data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/README.md +0 -231
  134. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/LICENSE +0 -29
  135. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/README.md +0 -484
  136. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/docs/jungle/README.md +0 -9
  137. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/docs/jungle/rc.d/README.md +0 -74
  138. data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/ChangeLog +0 -846
  139. data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/README.ja.rdoc +0 -58
  140. data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/README.rdoc +0 -60
  141. data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/CHANGELOG.md +0 -1346
  142. data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/MIT-LICENSE +0 -20
  143. data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/README.md +0 -384
  144. data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/Changelog.md +0 -101
  145. data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/LICENSE +0 -20
  146. data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/README.markdown +0 -227
  147. data/vendor/bundle/ruby/4.0.0/gems/rake-13.4.2/MIT-LICENSE +0 -21
  148. data/vendor/bundle/ruby/4.0.0/gems/rake-13.4.2/README.rdoc +0 -155
  149. data/vendor/bundle/ruby/4.0.0/gems/rake-release-1.4.0/LICENSE +0 -21
  150. data/vendor/bundle/ruby/4.0.0/gems/rake-release-1.4.0/README.md +0 -107
  151. data/vendor/bundle/ruby/4.0.0/gems/regexp_parser-2.12.0/LICENSE +0 -22
  152. data/vendor/bundle/ruby/4.0.0/gems/rexml-3.4.4/LICENSE.txt +0 -22
  153. data/vendor/bundle/ruby/4.0.0/gems/rexml-3.4.4/README.md +0 -57
  154. data/vendor/bundle/ruby/4.0.0/gems/rspec-3.13.2/LICENSE.md +0 -27
  155. data/vendor/bundle/ruby/4.0.0/gems/rspec-3.13.2/README.md +0 -47
  156. data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/Changelog.md +0 -2447
  157. data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/LICENSE.md +0 -26
  158. data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/README.md +0 -389
  159. data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/Changelog.md +0 -1366
  160. data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/LICENSE.md +0 -25
  161. data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/README.md +0 -326
  162. data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/Changelog.md +0 -1351
  163. data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/LICENSE.md +0 -25
  164. data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/README.md +0 -465
  165. data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/Changelog.md +0 -444
  166. data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/LICENSE.md +0 -23
  167. data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/README.md +0 -40
  168. data/vendor/bundle/ruby/4.0.0/gems/rubocop-1.88.0/LICENSE.txt +0 -20
  169. data/vendor/bundle/ruby/4.0.0/gems/rubocop-1.88.0/README.md +0 -255
  170. data/vendor/bundle/ruby/4.0.0/gems/rubocop-ast-1.49.1/LICENSE.txt +0 -20
  171. data/vendor/bundle/ruby/4.0.0/gems/rubocop-ast-1.49.1/README.md +0 -54
  172. data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/CHANGELOG.md +0 -116
  173. data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/MIT-LICENSE.md +0 -21
  174. data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/README.md +0 -91
  175. data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/CHANGELOG.md +0 -135
  176. data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/MIT-LICENSE.md +0 -21
  177. data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/README.md +0 -91
  178. data/vendor/bundle/ruby/4.0.0/gems/rubocop-performance-1.26.1/LICENSE.txt +0 -20
  179. data/vendor/bundle/ruby/4.0.0/gems/rubocop-performance-1.26.1/README.md +0 -100
  180. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/CHANGELOG.md +0 -1136
  181. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/MIT-LICENSE.md +0 -21
  182. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/README.md +0 -112
  183. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/CHANGELOG.md +0 -98
  184. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/MIT-LICENSE.md +0 -21
  185. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/README.md +0 -93
  186. data/vendor/bundle/ruby/4.0.0/gems/ruby-progressbar-1.13.0/LICENSE.txt +0 -19
  187. data/vendor/bundle/ruby/4.0.0/gems/ruby-progressbar-1.13.0/README.md +0 -131
  188. data/vendor/bundle/ruby/4.0.0/gems/securerandom-0.4.1/README.md +0 -72
  189. data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/CHANGELOG.md +0 -191
  190. data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/LICENSE +0 -20
  191. data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/README.md +0 -974
  192. data/vendor/bundle/ruby/4.0.0/gems/simplecov-cobertura-3.2.0/LICENSE +0 -202
  193. data/vendor/bundle/ruby/4.0.0/gems/simplecov-cobertura-3.2.0/README.md +0 -65
  194. data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/CHANGELOG.md +0 -114
  195. data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/LICENSE +0 -20
  196. data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/README.md +0 -30
  197. data/vendor/bundle/ruby/4.0.0/gems/simplecov_json_formatter-0.1.4/CHANGELOG.md +0 -13
  198. data/vendor/bundle/ruby/4.0.0/gems/simplecov_json_formatter-0.1.4/README.md +0 -29
  199. data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/CHANGELOG.md +0 -465
  200. data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/LICENSE +0 -22
  201. data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/README.md +0 -588
  202. data/vendor/bundle/ruby/4.0.0/gems/tzinfo-2.0.6/LICENSE +0 -19
  203. data/vendor/bundle/ruby/4.0.0/gems/tzinfo-2.0.6/README.md +0 -406
  204. data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/CHANGELOG.md +0 -299
  205. data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/MIT-LICENSE.txt +0 -22
  206. data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/README.md +0 -194
  207. data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/CHANGELOG.md +0 -202
  208. data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/MIT-LICENSE.txt +0 -20
  209. data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/README.md +0 -205
  210. data/vendor/bundle/ruby/4.0.0/gems/uri-1.1.1/README.md +0 -55
  211. data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/CHANGELOG.md +0 -2148
  212. data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/LICENSE +0 -20
  213. data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/README.md +0 -1229
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rack/media_type'
3
4
  require 'rack/utils'
4
5
  require 'json'
5
6
 
@@ -30,6 +31,10 @@ module Restify
30
31
 
31
32
  # Response body as string.
32
33
  #
34
+ # The body is encoded according to the charset from the response's
35
+ # `Content-Type` header. Without a charset, or with one unknown to
36
+ # Ruby, the body is returned as binary.
37
+ #
33
38
  # @return [String] Response body.
34
39
  #
35
40
  attr_reader :body
@@ -81,7 +86,7 @@ module Restify
81
86
  @code = code
82
87
  @status = STATUS_CODE_TO_SYMBOL[code]
83
88
  @headers = convert_headers(headers)
84
- @body = body
89
+ @body = encode_body(body)
85
90
  @message = Rack::Utils::HTTP_STATUS_CODES[code]
86
91
  end
87
92
 
@@ -112,6 +117,24 @@ module Restify
112
117
  headers['CONTENT_TYPE']
113
118
  end
114
119
 
120
+ # Return the encoding from the response's content type, if any.
121
+ #
122
+ # @return [Encoding, nil] Encoding, or nil if the content type
123
+ # carries no charset or an encoding unknown to Ruby.
124
+ #
125
+ def charset
126
+ return @charset if defined?(@charset)
127
+
128
+ charset = Rack::MediaType.params(content_type.to_s)['charset']
129
+
130
+ @charset = begin
131
+ Encoding.find(charset) if charset
132
+ rescue ArgumentError
133
+ # Unknown: keep the body as binary rather than guessing.
134
+ nil
135
+ end
136
+ end
137
+
115
138
  # Check if response is successful e.g. the status code
116
139
  # is on of 2XX.
117
140
  #
@@ -130,13 +153,20 @@ module Restify
130
153
  (400...600).cover? code
131
154
  end
132
155
 
156
+ # Decoded body for error messages, or nil if the body is empty or
157
+ # could not be parsed.
158
+ #
133
159
  # @api private
134
160
  def decoded_body
135
- @decoded_body ||= begin
161
+ return @decoded_body if defined?(@decoded_body)
162
+
163
+ @decoded_body = begin
136
164
  case content_type
137
165
  when %r{\Aapplication/json($|;)}
138
- ::JSON.parse(body)
166
+ ::JSON.parse(body) unless body.nil? || body.empty?
139
167
  end
168
+ rescue ::JSON::ParserError
169
+ nil
140
170
  end
141
171
  end
142
172
 
@@ -147,6 +177,12 @@ module Restify
147
177
 
148
178
  private
149
179
 
180
+ def encode_body(body)
181
+ return body unless body && charset
182
+
183
+ (+body).force_encoding(charset)
184
+ end
185
+
150
186
  def convert_headers(headers)
151
187
  headers.each.to_h do |pair|
152
188
  [pair[0].upcase, pair[1]]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'hitimes'
3
+ require 'timeout'
4
4
 
5
5
  module Restify
6
6
  class Timeout
@@ -14,9 +14,7 @@ module Restify
14
14
  def initialize(timeout, target = nil)
15
15
  @timeout = parse_timeout(timeout)
16
16
  @target = target
17
-
18
- @interval = ::Hitimes::Interval.new
19
- @interval.start
17
+ @deadline = now + @timeout
20
18
  end
21
19
 
22
20
  def wait_on!(ivar)
@@ -31,6 +29,10 @@ module Restify
31
29
  raise self if wait_interval <= 0
32
30
  end
33
31
 
32
+ def remaining
33
+ wait_interval
34
+ end
35
+
34
36
  def exception
35
37
  Error.new(@target)
36
38
  end
@@ -38,7 +40,11 @@ module Restify
38
40
  private
39
41
 
40
42
  def wait_interval
41
- @timeout - @interval.to_f
43
+ @deadline - now
44
+ end
45
+
46
+ def now
47
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
42
48
  end
43
49
 
44
50
  def parse_timeout(value)
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Restify
4
4
  module VERSION
5
- MAJOR = 2
6
- MINOR = 1
5
+ MAJOR = 3
6
+ MINOR = 0
7
7
  PATCH = 0
8
- STAGE = nil
8
+ STAGE = :rc2
9
9
  STRING = [MAJOR, MINOR, PATCH, STAGE].compact.join('.').freeze
10
10
 
11
11
  def self.to_s
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'restify'
4
+ require 'webmock'
5
+
6
+ module WebMock
7
+ module HttpLibAdapters
8
+ #
9
+ # WebMock support for Restify (independent of the adapter)
10
+ #
11
+ # Requests are intercepted before they are handed over to an
12
+ # adapter. Stubbed responses are returned as they are, including
13
+ # redirects, like WebMock does for other libcurl-based libraries.
14
+ # Requests not stubbed are passed on to the adapter, if allowed.
15
+ #
16
+ # Must be loaded before WebMock is enabled, e.g. in
17
+ # `spec_helper.rb`:
18
+ #
19
+ # require 'webmock/rspec'
20
+ # require 'restify/webmock'
21
+ #
22
+ class RestifyAdapter < HttpLibAdapter
23
+ adapter_for :restify
24
+
25
+ class << self
26
+ def enable!
27
+ @enabled = true
28
+ end
29
+
30
+ def disable!
31
+ @enabled = false
32
+ end
33
+
34
+ def enabled?
35
+ @enabled == true
36
+ end
37
+
38
+ # Respond to a request with a stubbed response, or pass it on to
39
+ # the given block if allowed.
40
+ #
41
+ # @api private
42
+ def call(request, &)
43
+ signature = signature(request)
44
+ RequestRegistry.instance.requested_signatures.put(signature)
45
+
46
+ stub = StubRegistry.instance.response_for_request(signature)
47
+ return passthrough(signature, &) unless stub
48
+
49
+ CallbackRegistry.invoke_callbacks({lib: :restify}, signature, stub)
50
+
51
+ Restify::Promise.fulfilled(respond(request, stub))
52
+ rescue Exception => e # rubocop:disable Lint/RescueException
53
+ # e.g. `NetConnectNotAllowedError`
54
+ Restify::Promise.rejected(e)
55
+ end
56
+
57
+ private
58
+
59
+ def signature(request)
60
+ RequestSignature.new(
61
+ request.method.to_sym,
62
+ request.uri.to_s,
63
+ body: request.body,
64
+ headers: request.headers,
65
+ )
66
+ end
67
+
68
+ def passthrough(signature)
69
+ unless ::WebMock.net_connect_allowed?(signature.uri)
70
+ raise NetConnectNotAllowedError.new(signature)
71
+ end
72
+
73
+ yield.tap do |promise|
74
+ # Observers run when the request is complete, even when
75
+ # nobody waits on the promise.
76
+ promise.add_observer do |_, response, _|
77
+ next unless response
78
+
79
+ CallbackRegistry.invoke_callbacks(
80
+ {lib: :restify, real_request: true},
81
+ signature,
82
+ webmock_response(response),
83
+ )
84
+ end
85
+ end
86
+ end
87
+
88
+ def respond(request, stub)
89
+ raise Restify::NetworkError.new(request, 'Timeout was reached') if stub.should_timeout
90
+
91
+ stub.raise_error_if_any
92
+
93
+ headers = (stub.headers || {}).to_h do |name, value|
94
+ [name.upcase.tr('-', '_'), value]
95
+ end
96
+
97
+ Restify::Response.new(request, request.uri, stub.status[0], headers, stub.body)
98
+ end
99
+
100
+ def webmock_response(response)
101
+ Response.new.tap do |webmock|
102
+ webmock.status = [response.code, response.message.to_s]
103
+ webmock.headers = response.headers.to_h do |name, value|
104
+ [name.split('_').map(&:capitalize).join('-'), value]
105
+ end
106
+ webmock.body = response.body.to_s
107
+ end
108
+ end
109
+ end
110
+
111
+ # Intercept requests after `Adapter::Telemetry` but before
112
+ # `Adapter::Base#call` so that telemetry is included.
113
+ #
114
+ # @api private
115
+ module Intercept
116
+ def call(request)
117
+ return super unless RestifyAdapter.enabled?
118
+
119
+ RestifyAdapter.call(request) { super }
120
+ end
121
+ end
122
+
123
+ ::Restify::Adapter::Telemetry.include(Intercept)
124
+ end
125
+ end
126
+ end
data/lib/restify.rb CHANGED
@@ -28,7 +28,11 @@ module Restify
28
28
 
29
29
  module Adapter
30
30
  require 'restify/adapter/base'
31
- require 'restify/adapter/typhoeus'
31
+ require 'restify/adapter/loop_lock'
32
+ require 'restify/adapter/ethon'
33
+
34
+ # Optional, requires the `typhoeus` gem.
35
+ autoload :Typhoeus, 'restify/adapter/typhoeus'
32
36
  end
33
37
 
34
38
  module Processors
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Adapter::Ethon do
6
+ describe 'completed transfers' do
7
+ subject(:value) do
8
+ Restify::Promise.create do |writer|
9
+ described_class.new.send(
10
+ :complete,
11
+ easy,
12
+ request,
13
+ writer,
14
+ )
15
+ end.value!
16
+ end
17
+
18
+ let(:request) { Restify::Request.new(uri: 'http://example.org/base') }
19
+ let(:return_code) { :ok }
20
+ let(:response_code) { 200 }
21
+ let(:effective_url) { 'http://example.org/base' }
22
+
23
+ # A transfer is handed back from libcurl with its return code and,
24
+ # for HTTP, the status code of the response.
25
+ def easy
26
+ instance_double(
27
+ described_class::Easy,
28
+ return_code:,
29
+ response_code:,
30
+ effective_url:,
31
+ response_headers: "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n",
32
+ response_body: '{}',
33
+ )
34
+ end
35
+
36
+ it 'fulfills the promise with the response' do
37
+ expect(value.code).to eq 200
38
+ expect(value.uri.to_s).to eq 'http://example.org/base'
39
+ end
40
+
41
+ context 'when redirected' do
42
+ let(:effective_url) { 'http://example.org/other/base' }
43
+
44
+ it 'uses the effective URL as the response URI' do
45
+ expect(value.uri.to_s).to eq 'http://example.org/other/base'
46
+ end
47
+ end
48
+
49
+ context 'when the transfer failed' do
50
+ let(:return_code) { :couldnt_connect }
51
+ let(:response_code) { 0 }
52
+
53
+ it 'rejects the promise' do
54
+ expect { value }.to raise_error Restify::NetworkError, /connect/i
55
+ end
56
+ end
57
+
58
+ # Non-HTTP protocols are refused before a transfer is started, but
59
+ # libcurl still reports success with a zero status code whenever no
60
+ # HTTP status was received, and no status code at all when it cannot
61
+ # be read back. Neither has a response to process.
62
+ context 'without an HTTP status' do
63
+ let(:response_code) { 0 }
64
+
65
+ it 'rejects the promise' do
66
+ expect { value }.to raise_error Restify::NetworkError, /without HTTP status/
67
+ end
68
+ end
69
+
70
+ context 'with an unavailable HTTP status' do
71
+ let(:response_code) { nil }
72
+
73
+ it 'rejects the promise' do
74
+ expect { value }.to raise_error Restify::NetworkError, /without HTTP status/
75
+ end
76
+ end
77
+
78
+ # Unexpected errors must reject the promise too, as the caller would
79
+ # otherwise keep waiting for it until it times out.
80
+ context 'when building the response fails' do
81
+ def easy
82
+ super.tap do |double|
83
+ allow(double).to receive(:response_headers).and_raise('kaboom')
84
+ end
85
+ end
86
+
87
+ it 'rejects the promise' do
88
+ expect { value }.to raise_error 'kaboom'
89
+ end
90
+ end
91
+ end
92
+
93
+ describe 'waiting on requests' do
94
+ let(:adapter) { described_class.new }
95
+
96
+ # Threads that completed a transfer, i.e. ran the event loop.
97
+ let(:completed_by) { Queue.new }
98
+
99
+ before do
100
+ stub_request(:get, 'http://stubserver/fast')
101
+ .to_return(status: 200, body: '{}')
102
+ stub_request(:get, 'http://stubserver/slow')
103
+ .to_return { sleep 0.5 and {status: 200, body: '{}'} }
104
+
105
+ allow(adapter).to receive(:complete).and_wrap_original do |m, *args|
106
+ completed_by << Thread.current
107
+ m.call(*args)
108
+ end
109
+ end
110
+
111
+ def request(path)
112
+ Restify::Request.new(uri: "http://localhost:9292/#{path}")
113
+ end
114
+
115
+ def completed_by_threads
116
+ Array.new(completed_by.size) { completed_by.pop }
117
+ end
118
+
119
+ it 'runs the event loop in the waiting thread' do
120
+ expect(adapter.call(request('fast')).value!.code).to eq 200
121
+ expect(completed_by_threads).to eq [Thread.current]
122
+ end
123
+
124
+ it 'completes requests nobody waits on in the background' do
125
+ adapter.call(request('fast'))
126
+
127
+ # Poll without waiting on the promise:
128
+ Timeout.timeout(1) { sleep 0.01 while completed_by.empty? }
129
+
130
+ expect(completed_by_threads).not_to include Thread.current
131
+ end
132
+
133
+ it 'times out without blocking other requests' do
134
+ expect { adapter.call(request('slow')).value!(0.1) }.to raise_error Timeout::Error
135
+ expect(adapter.call(request('fast')).value!.code).to eq 200
136
+ end
137
+
138
+ it 'processes requests from many threads' do
139
+ threads = Array.new(8) do
140
+ Thread.new do
141
+ Array.new(5) { adapter.call(request('fast')).value!.code }
142
+ end
143
+ end
144
+
145
+ expect(threads.flat_map(&:value)).to all eq 200
146
+ end
147
+
148
+ # Only the adapter's own promises are completed by whichever thread
149
+ # runs the event loop. Callbacks chained to them must still run in
150
+ # the thread waiting on them, and only when waiting on them.
151
+ describe 'chained callbacks' do
152
+ it 'does not run them before waiting on them' do
153
+ ran_in = Queue.new
154
+ promise = adapter.call(request('fast'))
155
+ chained = promise.then { ran_in << Thread.current }
156
+
157
+ promise.value!
158
+ expect(ran_in).to be_empty
159
+
160
+ chained.value!
161
+ expect(ran_in.pop(timeout: 1)).to eq Thread.current
162
+ end
163
+
164
+ it 'runs them in the waiting thread when another thread runs the loop' do
165
+ # Keep the loop busy in another thread, until after the fast
166
+ # request completed.
167
+ driver = Thread.new { adapter.call(request('slow')).value! }
168
+ sleep 0.1
169
+
170
+ chained = adapter.call(request('fast')).then do |response|
171
+ [Thread.current, response.code]
172
+ end
173
+
174
+ expect(chained.value!).to eq [Thread.current, 200]
175
+ expect(completed_by_threads.first).to eq driver
176
+ ensure
177
+ driver&.join
178
+ end
179
+ end
180
+
181
+ context 'when waiting from within the event loop' do
182
+ it 'does not run the loop again' do
183
+ adapter.instance_variable_get(:@loop).acquire(0.1)
184
+
185
+ expect(adapter.drive(Restify::Promise.new, Restify::Timeout.new(0.1))).to be false
186
+ end
187
+ end
188
+ end
189
+
190
+ describe 'after fork' do
191
+ let(:adapter) { described_class.new }
192
+ let(:connects) { Queue.new }
193
+
194
+ before do
195
+ stub_request(:get, 'http://stubserver/fast')
196
+ .to_return(status: 200, body: '{}')
197
+ stub_request(:get, 'http://stubserver/slow')
198
+ .to_return { sleep 0.5 and {status: 200, body: '{}'} }
199
+
200
+ allow(adapter).to receive(:complete).and_wrap_original do |m, easy, *args|
201
+ connects << Ethon::Curl.get_info_long(:num_connects, easy.handle)
202
+ m.call(easy, *args)
203
+ end
204
+ end
205
+
206
+ def request(path)
207
+ Restify::Request.new(uri: "http://localhost:9292/#{path}")
208
+ end
209
+
210
+ def in_child
211
+ reader, writer = IO.pipe
212
+
213
+ pid = fork do
214
+ reader.close
215
+ result = begin
216
+ yield
217
+ rescue Exception => e # rubocop:disable Lint/RescueException
218
+ e
219
+ end
220
+ writer.write(Marshal.dump(result))
221
+ ensure
222
+ # Skip at exit handlers of the parent
223
+ exit!(0)
224
+ end
225
+
226
+ writer.close
227
+ Marshal.load(reader.read) # rubocop:disable Security/MarshalLoad
228
+ ensure
229
+ reader&.close
230
+ Process.wait(pid) if pid
231
+ end
232
+
233
+ it 'does not use connections of the parent process' do
234
+ # The parent process pools and reuses its connection.
235
+ 2.times { adapter.call(request('fast')).value! }
236
+ expect(Array.new(2) { connects.pop(timeout: 1) }).to eq [1, 0]
237
+
238
+ result = in_child do
239
+ code = adapter.call(request('fast')).value!.code
240
+ [code, connects.pop(timeout: 1)]
241
+ end
242
+
243
+ # A new connection, not the one pooled by the parent process
244
+ expect(result).to eq [200, 1]
245
+
246
+ # The parent process can still use its connections
247
+ expect(adapter.call(request('fast')).value!.code).to eq 200
248
+ end
249
+
250
+ it 'rejects requests started before the fork in the child process' do
251
+ promise = adapter.call(request('slow'))
252
+
253
+ result = in_child do
254
+ promise.value!
255
+ rescue Restify::NetworkError => e
256
+ e.message
257
+ end
258
+
259
+ expect(result).to include 'before fork'
260
+ expect(promise.value!.code).to eq 200
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Adapter::LoopLock do
6
+ subject(:lock) { described_class.new { interrupts << Thread.current } }
7
+
8
+ let(:interrupts) { Queue.new }
9
+ let(:threads) { [] }
10
+
11
+ after { threads.each(&:kill).each(&:join) }
12
+
13
+ # Run the block in another thread, and wait until it has returned.
14
+ # The thread is kept alive until the example finished.
15
+ def other(&)
16
+ done = Concurrent::Event.new
17
+ threads << Thread.new do
18
+ yield
19
+ done.set
20
+ sleep
21
+ end
22
+ expect(done.wait(1)).to be true
23
+ end
24
+
25
+ def elapsed
26
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
+ yield
28
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
29
+ end
30
+
31
+ describe '#acquire' do
32
+ it 'acquires a free loop' do
33
+ expect(lock.acquire(1)).to be true
34
+ expect(lock).to be_owned
35
+ end
36
+
37
+ it 'does not acquire the loop when done' do
38
+ expect(lock.acquire(1) { true }).to be false
39
+ expect(lock).not_to be_owned
40
+ end
41
+
42
+ it 'times out while another thread holds the loop' do
43
+ other { lock.acquire(1) }
44
+
45
+ expect(elapsed { expect(lock.acquire(0.1)).to be false }).to be >= 0.1
46
+ expect(lock).not_to be_owned
47
+ end
48
+
49
+ it 'waits for another thread to release the loop' do
50
+ acquired = Concurrent::Event.new
51
+ released = false
52
+ threads << Thread.new do
53
+ lock.acquire(1)
54
+ acquired.set
55
+ sleep 0.1
56
+ released = true
57
+ lock.release
58
+ end
59
+ expect(acquired.wait(1)).to be true
60
+
61
+ expect(lock.acquire(1)).to be true
62
+ expect(released).to be true
63
+ end
64
+
65
+ it 'stops waiting when done after being notified' do
66
+ done = false
67
+ other { lock.acquire(1) }
68
+ threads << Thread.new do
69
+ sleep 0.1
70
+ done = true
71
+ lock.notify
72
+ end
73
+
74
+ expect(elapsed { expect(lock.acquire(1) { done }).to be false }).to be < 0.5
75
+ end
76
+
77
+ it 'treats a dead thread as not holding the loop' do
78
+ Thread.new { lock.acquire(1) }.join
79
+
80
+ expect(lock.acquire(0.1)).to be true
81
+ end
82
+ end
83
+
84
+ describe '#release' do
85
+ it 'lets a waiting thread acquire the loop' do
86
+ lock.acquire(1)
87
+ waiter = Thread.new { lock.acquire(1) }
88
+ sleep 0.05
89
+
90
+ lock.release
91
+
92
+ expect(waiter.join(1)&.value).to be true
93
+ expect(lock).not_to be_owned
94
+ end
95
+ end
96
+
97
+ describe 'with a background thread' do
98
+ let(:background) { Queue.new }
99
+
100
+ # A background thread running the loop until interrupted, like an
101
+ # event loop waiting for sockets. Blocks on `interrupts` on purpose,
102
+ # and is killed after each example.
103
+ def start_background
104
+ threads << Thread.new do
105
+ loop do
106
+ lock.acquire_background
107
+ background << :acquired
108
+ interrupts.pop
109
+ lock.release
110
+ end
111
+ end
112
+
113
+ expect(background.pop(timeout: 1)).to eq :acquired
114
+ end
115
+
116
+ it 'interrupts the background thread to release the loop' do
117
+ start_background
118
+
119
+ expect(lock.acquire(1)).to be true
120
+ expect(background).to be_empty
121
+ end
122
+
123
+ it 'blocks the background thread while a thread holds the loop' do
124
+ start_background
125
+ lock.acquire(1)
126
+
127
+ sleep 0.1
128
+ expect(background).to be_empty
129
+
130
+ lock.release
131
+ expect(background.pop(timeout: 1)).to eq :acquired
132
+ end
133
+ end
134
+
135
+ describe '#reset!' do
136
+ it 'forgets threads holding the loop' do
137
+ other { lock.acquire(1) }
138
+
139
+ lock.reset!
140
+
141
+ expect(lock.acquire(0.1)).to be true
142
+ end
143
+ end
144
+ end