nonnative 3.26.0 → 3.29.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59db521cea52e4a8c1d2aa8d76e79b50e7a9b76876b63ed8be4e46a665614a5d
4
- data.tar.gz: 45f21fd21830aab99ce440d65d7cb80720449169684320f814a9e767881d8a1f
3
+ metadata.gz: d81874eb81ecd99758c560530adaf6dd701ac6136258ec36b04a074312e45e65
4
+ data.tar.gz: 05e90f242c9cd70a7c544705d2db49c593e7615ff7edfc28e6864154036e758b
5
5
  SHA512:
6
- metadata.gz: 47d05d9b12dc2601470b87af8a880018a0be1808aee7f501f48a703db422b57db37d041b4f93fbb4ebb5afd4545bccf1ed3bf9b15b7a9d9909ca9134dc1d0111
7
- data.tar.gz: 0d191245bdedd04c73096cfc4c3cd1871966c6aa9b09a1e5295c835a76798afea4633638f2d6f3bc1179d20f683484c6b033a8c655b45c047c9c468d18aa7fca
6
+ metadata.gz: 59abcf2ef7f4c1cf0785ab43dbca7af059441f8a65379601adc1204c8860955b462fc7fa8dac8b1bbb93cd7a449296cac36ea18bee692b67baad567b83cea3ad
7
+ data.tar.gz: ac4407b3205ca54a8e2496e9e2ce2594ee8bf8d7fff8f9c0ae5bce929fdf4b707732c54c8b45ccb038e79bb34601cf52823a7ee99ae6c7ae05a8bbdfb60cc05a
data/README.md CHANGED
@@ -852,6 +852,7 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
852
852
  - `timeout` - Accepts the connection and stalls traffic until reset or stop closes the connection, so clients exercise their own read timeout behavior.
853
853
  - `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
854
854
  - `bandwidth` - Throttles forwarded throughput to `options.rate` kilobytes per second (1 KB = 1024 bytes) by sleeping in proportion to the bytes read, in both directions, so clients see a slow-but-alive dependency. When `rate` is absent or not positive, traffic forwards at full speed.
855
+ - `limit_data` - Forwards client requests unchanged, then sends the first `options.bytes` bytes of the upstream byte stream on each connection and gracefully closes the connection. When `bytes` is absent or not positive, traffic forwards at full speed.
855
856
 
856
857
  ###### 🧩 Fault Injection Services
857
858
 
@@ -864,6 +865,7 @@ service = Nonnative.pool.service_by_name(name)
864
865
  service.proxy.close_all # To use close_all.
865
866
  service.proxy.reset_peer # To reset (RST) client connections.
866
867
  service.proxy.timeout # To stall traffic until reset or stop.
868
+ service.proxy.limit_data # To truncate the upstream byte stream at options.bytes.
867
869
  service.proxy.reset # To reset it back to a good state.
868
870
  ```
869
871
 
@@ -873,6 +875,7 @@ Use the Cucumber proxy steps:
873
875
  Given I set the proxy for service 'service_1' to 'close_all'
874
876
  Given I set the proxy for service 'service_1' to 'reset_peer'
875
877
  Given I set the proxy for service 'service_1' to 'timeout'
878
+ Given I set the proxy for service 'service_1' to 'limit_data'
876
879
  Then I should reset the proxy for service 'service_1'
877
880
  ```
878
881
 
@@ -60,6 +60,7 @@ module Nonnative
60
60
  'timeout' => :timeout,
61
61
  'invalid_data' => :invalid_data,
62
62
  'bandwidth' => :bandwidth,
63
+ 'limit_data' => :limit_data,
63
64
  'reset' => :reset
64
65
  }.freeze
65
66
 
@@ -15,6 +15,7 @@ module Nonnative
15
15
  # - {#timeout}: accept connections and keep them silent until clients time out
16
16
  # - {#invalid_data}: forward requests unchanged and mutate upstream responses before they reach clients
17
17
  # - {#bandwidth}: throttle forwarded throughput to a configured rate (KB/s)
18
+ # - {#limit_data}: forward a configured number of response bytes, then gracefully close
18
19
  # - {#reset}: return to healthy pass-through behavior
19
20
  #
20
21
  # State changes terminate any active connections so new connections observe the new behavior.
@@ -36,6 +37,8 @@ module Nonnative
36
37
  # - `delay`: delay duration in seconds used by {#delay}
37
38
  # - `jitter`: optional random offset (seconds) added in `-jitter..jitter` to each `delay` (a
38
39
  # negative value uses its magnitude), so clients see variable latency instead of a flat value
40
+ # - `bytes`: positive response byte limit used by {#limit_data}; absent or non-positive values
41
+ # use pass-through behavior
39
42
  #
40
43
  # @see Nonnative::Proxy
41
44
  # @see Nonnative::SocketPairFactory
@@ -150,6 +153,17 @@ module Nonnative
150
153
  apply_state :bandwidth
151
154
  end
152
155
 
156
+ # Truncates the upstream byte stream after a configured number of bytes.
157
+ #
158
+ # Client requests are forwarded unchanged. The response byte limit is read from
159
+ # `service.proxy.options[:bytes]`; when it is absent or not positive, the connection forwards at
160
+ # full speed without truncation.
161
+ #
162
+ # @return [void]
163
+ def limit_data
164
+ apply_state :limit_data
165
+ end
166
+
153
167
  # Resets the proxy back to healthy pass-through behavior.
154
168
  #
155
169
  # @return [void]
@@ -92,19 +92,45 @@ module Nonnative
92
92
  end
93
93
  end
94
94
 
95
+ # Performs a HEAD request.
96
+ #
97
+ # @param pathname [String] path relative to `host`
98
+ # @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
99
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
100
+ def head(pathname, opts = {})
101
+ with_exception do
102
+ resource(pathname, opts).head
103
+ end
104
+ end
105
+
106
+ # Performs an OPTIONS request.
107
+ #
108
+ # @param pathname [String] path relative to `host`
109
+ # @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
110
+ # @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
111
+ def options(pathname, opts = {})
112
+ with_exception do
113
+ RestClient::Request.execute(opts.merge(method: :options, url: request_url(pathname)))
114
+ end
115
+ end
116
+
95
117
  # Creates a RestClient resource for a relative path.
96
118
  #
97
119
  # @param pathname [String] path relative to `host`
98
120
  # @param opts [Hash] RestClient request options
99
121
  # @return [RestClient::Resource]
100
122
  def resource(pathname, opts)
101
- RestClient::Resource.new(URI.join(host, pathname).to_s, opts)
123
+ RestClient::Resource.new(request_url(pathname), opts)
102
124
  end
103
125
 
104
126
  private
105
127
 
106
128
  attr_reader :host, :exceptions
107
129
 
130
+ def request_url(pathname)
131
+ URI.join(host, pathname).to_s
132
+ end
133
+
108
134
  def with_exception
109
135
  yield
110
136
  rescue *exceptions => e
@@ -22,7 +22,7 @@ module Nonnative
22
22
  #
23
23
  # The upstream host is configured via service settings (see {Nonnative::HTTPProxyServer}).
24
24
  #
25
- # Supported HTTP verbs: GET, POST, PUT, PATCH, DELETE.
25
+ # Supported HTTP verbs: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS.
26
26
  class HTTPProxy < Nonnative::HTTPService
27
27
  NON_FORWARDABLE_RESPONSE_HEADERS = %w[
28
28
  connection
@@ -145,7 +145,16 @@ module Nonnative
145
145
  header.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
146
146
  end
147
147
 
148
- %w[get post put patch delete].each do |verb|
148
+ # Registered before `get` so it takes precedence over Sinatra's GET-generated HEAD route,
149
+ # which would otherwise forward HEAD requests upstream as GET.
150
+ head(/.*/) do
151
+ res = api_response(method: :head, url: build_url(request, settings), headers: forward_request_headers(request))
152
+
153
+ headers(forward_response_headers(res))
154
+ status res.code
155
+ end
156
+
157
+ %w[get post put patch delete options].each do |verb|
149
158
  send(verb, /.*/) do
150
159
  res = api_response(
151
160
  method: verb.to_sym,
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Socket-pair variant used by the fault-injection proxy to truncate upstream responses.
5
+ #
6
+ # When active, client requests pass through unchanged, while the first `proxy.options[:bytes]`
7
+ # bytes of the upstream stream are forwarded to the client on each connection. Once the limit is
8
+ # reached, the pair's normal connection lifecycle closes both sockets gracefully. A missing or
9
+ # non-positive limit leaves the connection in pass-through mode.
10
+ #
11
+ # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#limit_data}.
12
+ #
13
+ # @see Nonnative::FaultInjectionProxy
14
+ # @see Nonnative::SocketPairFactory
15
+ # @see Nonnative::SocketPair
16
+ class LimitDataSocketPair < SocketPair
17
+ # Tracks the client socket and response byte budget for this connection.
18
+ #
19
+ # @param local_socket [TCPSocket] the accepted client socket
20
+ # @return [void]
21
+ def connect(local_socket)
22
+ @local_socket = local_socket
23
+ @remaining = proxy.options[:bytes]
24
+ @limit_reached = false
25
+
26
+ super
27
+ ensure
28
+ @local_socket = nil
29
+ @remaining = nil
30
+ @limit_reached = nil
31
+ end
32
+
33
+ protected
34
+
35
+ # Stops the base forwarding loop after the response budget has been written.
36
+ #
37
+ # @param ready [Array<Array<IO>>] the result from `select`
38
+ # @param source_socket [IO] readable side
39
+ # @param destination_socket [IO] writable side
40
+ # @return [Boolean] whether the forwarding loop should terminate
41
+ def pipe?(ready, source_socket, destination_socket)
42
+ return true if @limit_reached
43
+
44
+ super || @limit_reached
45
+ end
46
+
47
+ # Limits writes to the client while leaving writes to the upstream unchanged.
48
+ #
49
+ # @param socket [IO] the socket to write to
50
+ # @param data [String] the original payload
51
+ # @return [Integer] number of bytes written
52
+ def write(socket, data)
53
+ return super unless socket.equal?(@local_socket)
54
+ return super if @remaining.nil? || @remaining <= 0
55
+
56
+ data = data.byteslice(0, @remaining)
57
+ @remaining -= data.bytesize
58
+ @limit_reached = @remaining.zero?
59
+
60
+ super
61
+ end
62
+ end
63
+ end
@@ -32,7 +32,14 @@ module Nonnative
32
32
  # - `true` (thread creation itself is considered started; readiness is checked separately)
33
33
  def start
34
34
  unless thread
35
- @thread = Thread.new { perform_start }
35
+ @error = nil
36
+ @thread = Thread.new do
37
+ perform_start
38
+ rescue StandardError => e
39
+ @error = e
40
+ raise
41
+ end
42
+ @thread.report_on_exception = true
36
43
 
37
44
  wait_start
38
45
 
@@ -61,8 +68,22 @@ module Nonnative
61
68
  object_id
62
69
  end
63
70
 
71
+ # Describes how the server thread terminated before becoming ready, for lifecycle diagnostics.
72
+ #
73
+ # Returns `nil` while the thread is still alive, so callers can distinguish a dead thread from a
74
+ # live server that merely missed its readiness window.
75
+ #
76
+ # @return [String, nil] termination detail (clean early return or uncaught exception), or `nil`
77
+ def termination
78
+ return if thread.nil? || thread.alive?
79
+
80
+ return "server thread raised #{error.class}: #{error.message}" if error
81
+
82
+ 'server thread exited before readiness'
83
+ end
84
+
64
85
  private
65
86
 
66
- attr_reader :thread, :timeout
87
+ attr_reader :thread, :timeout, :error
67
88
  end
68
89
  end
@@ -14,6 +14,7 @@ module Nonnative
14
14
  # - `:timeout` -> {Nonnative::TimeoutSocketPair}
15
15
  # - `:invalid_data` -> {Nonnative::InvalidDataSocketPair}
16
16
  # - `:bandwidth` -> {Nonnative::BandwidthSocketPair}
17
+ # - `:limit_data` -> {Nonnative::LimitDataSocketPair}
17
18
  #
18
19
  # @see Nonnative::FaultInjectionProxy
19
20
  # @see Nonnative::SocketPair
@@ -23,30 +24,28 @@ module Nonnative
23
24
  # @see Nonnative::TimeoutSocketPair
24
25
  # @see Nonnative::InvalidDataSocketPair
25
26
  # @see Nonnative::BandwidthSocketPair
27
+ # @see Nonnative::LimitDataSocketPair
26
28
  class SocketPairFactory
29
+ PAIR_BY_STATE = {
30
+ close_all: CloseAllSocketPair,
31
+ reset_peer: ResetPeerSocketPair,
32
+ delay: DelaySocketPair,
33
+ timeout: TimeoutSocketPair,
34
+ invalid_data: InvalidDataSocketPair,
35
+ bandwidth: BandwidthSocketPair,
36
+ limit_data: LimitDataSocketPair
37
+ }.freeze
38
+ private_constant :PAIR_BY_STATE
39
+
27
40
  class << self
28
41
  # Creates a socket-pair instance for the given proxy state.
29
42
  #
30
- # @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:reset_peer`, `:delay`, `:timeout`, `:invalid_data`, `:bandwidth`)
43
+ # @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:reset_peer`, `:delay`, `:timeout`,
44
+ # `:invalid_data`, `:bandwidth`, `:limit_data`)
31
45
  # @param proxy [Nonnative::ConfigurationProxy] proxy configuration (host/port/options)
32
46
  # @return [Nonnative::SocketPair] a socket-pair implementation instance
33
47
  def create(kind, proxy)
34
- pair = case kind
35
- when :close_all
36
- CloseAllSocketPair
37
- when :reset_peer
38
- ResetPeerSocketPair
39
- when :delay
40
- DelaySocketPair
41
- when :timeout
42
- TimeoutSocketPair
43
- when :invalid_data
44
- InvalidDataSocketPair
45
- when :bandwidth
46
- BandwidthSocketPair
47
- else
48
- SocketPair
49
- end
48
+ pair = PAIR_BY_STATE.fetch(kind, SocketPair)
50
49
 
51
50
  pair.new(proxy)
52
51
  end
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.26.0'
7
+ VERSION = '3.29.0'
8
8
  end
data/lib/nonnative.rb CHANGED
@@ -116,6 +116,7 @@ require 'nonnative/delay_socket_pair'
116
116
  require 'nonnative/timeout_socket_pair'
117
117
  require 'nonnative/invalid_data_socket_pair'
118
118
  require 'nonnative/bandwidth_socket_pair'
119
+ require 'nonnative/limit_data_socket_pair'
119
120
  require 'nonnative/socket_pair_factory'
120
121
  require 'nonnative/go_executable'
121
122
  require 'nonnative/cucumber'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nonnative
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.26.0
4
+ version: 3.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski
@@ -422,6 +422,7 @@ files:
422
422
  - lib/nonnative/http_service.rb
423
423
  - lib/nonnative/invalid_data_socket_pair.rb
424
424
  - lib/nonnative/jwt_token.rb
425
+ - lib/nonnative/limit_data_socket_pair.rb
425
426
  - lib/nonnative/no_proxy.rb
426
427
  - lib/nonnative/not_found_error.rb
427
428
  - lib/nonnative/observability.rb