workos 7.1.2 → 7.1.3

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: 6e48d8fb142eca494c5b316c17dd940cfbb2244a5c1589a9b30cc7106f850631
4
- data.tar.gz: 1fd7a0f9e75d74109e01e6a54546cf7f83155c78e64ea321e77d032e219a749c
3
+ metadata.gz: 4ec14077a9e698e156af30d076db74ffa4b6170a551ef33ccd9e9ae540762805
4
+ data.tar.gz: ccc00996a607b696d4bb7768c943161d9b1634c7bb1ce02b0d3005d452d92ca4
5
5
  SHA512:
6
- metadata.gz: 29c7c96477e7d66be140bee056673e9d5385beb15da8cad3011e9a3335d8d6ebcf902d9547c2188656ed0fdb0cdde724df79a6eb240507e501a46c6c0cded34e
7
- data.tar.gz: 34592fb5ecd1d1e52f06338115f3740a02304f1c3d077c06a8038bb8bb009dd769073d2bff0bf782558a945bd3ff9b4270c868a1101ebf2c7ac6443bbaf735cd
6
+ metadata.gz: 32a6baa84226e9fcb7606e7ee697547c42a90895a7fa59e1c4834270b84eeb1a08265e4d8ab5e650e9137262697ae984b3352c4eead1b77ff35257210eb6b3a7
7
+ data.tar.gz: 998eaf812e99674651881d09cf9ff7ad658e380198c4aa69c6b2c48e768e5f8ff4cefd0ed59ff30ecf8f653fc8e0a5337ab653e1dc2f3a19f5cfeb5c235c91ae
@@ -3,7 +3,7 @@ name: Release Please
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - main
6
+ - v7.1.x
7
7
 
8
8
  permissions:
9
9
  contents: write
@@ -24,6 +24,7 @@ jobs:
24
24
  id: release
25
25
  with:
26
26
  token: ${{ steps.generate-token.outputs.token }}
27
+ target-branch: v7.1.x
27
28
 
28
29
  - name: Update Gemfile.lock on release PR
29
30
  if: steps.release.outputs.pr
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "7.1.2"
2
+ ".": "7.1.3"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.1.3](https://github.com/workos/workos-ruby/compare/v7.1.2...v7.1.3) (2026-08-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * explicitly evict connections if request does not complete as success ([853de67](https://github.com/workos/workos-ruby/commit/853de6740a8038bde3ac1625fde98a20ea87604e))
9
+
3
10
  ## [7.1.2](https://github.com/workos/workos-ruby/compare/v7.1.1...v7.1.2) (2026-05-06)
4
11
 
5
12
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- workos (7.1.2)
4
+ workos (7.1.3)
5
5
  jwt (~> 3.1)
6
6
  logger (~> 1.7)
7
7
  zeitwerk (~> 2.6)
@@ -124,7 +124,7 @@ CHECKSUMS
124
124
  unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
125
125
  unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
126
126
  webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
127
- workos (7.1.2)
127
+ workos (7.1.3)
128
128
  zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
129
129
 
130
130
  BUNDLED WITH
@@ -136,7 +136,20 @@ module WorkOS
136
136
  loop do
137
137
  log(:debug, "request start", method: request.method, path: request.path, attempt: attempt + 1)
138
138
  http = connection_for(base, timeout)
139
- response = http.request(request)
139
+ request_completed = false
140
+ begin
141
+ response = http.request(request)
142
+ request_completed = true
143
+ ensure
144
+ # Any exit from #request other than a returned response can leave
145
+ # the socket mid-stream: a connection error the rescue below knows
146
+ # about, one it doesn't (OpenSSL::SSL::SSLError,
147
+ # Net::HTTPBadResponse), or a non-local exit such as an
148
+ # application-level Timeout.timeout or Thread#kill. A half-read
149
+ # socket handed back to the pool desyncs the *next* request on this
150
+ # thread, so drop it here rather than in the rescue.
151
+ evict_connection(base) unless request_completed
152
+ end
140
153
  return response if response.is_a?(Net::HTTPSuccess)
141
154
 
142
155
  if attempt < retries && retryable?(response)
@@ -2,5 +2,5 @@
2
2
 
3
3
  # @oagen-ignore-file
4
4
  module WorkOS
5
- VERSION = "7.1.2"
5
+ VERSION = "7.1.3"
6
6
  end
@@ -82,10 +82,36 @@ class BaseClientTest < Minitest::Test
82
82
  end
83
83
  end
84
84
 
85
+ # A pooled connection that either returns a canned response or raises when
86
+ # driven, so execute_request can be exercised without real TCP+TLS.
87
+ class StubConnection < FakeConnection
88
+ attr_accessor :read_timeout, :open_timeout
89
+
90
+ def initialize(response: nil, error: nil, **kwargs)
91
+ super(**kwargs)
92
+ @response = response
93
+ @error = error
94
+ end
95
+
96
+ def request(_request)
97
+ raise @error if @error
98
+
99
+ @response
100
+ end
101
+ end
102
+
85
103
  def setup
86
104
  @client = WorkOS::BaseClient.new(api_key: "sk_test_123", max_retries: 1)
87
105
  end
88
106
 
107
+ def teardown
108
+ super
109
+ # Close any open connections and clear the fiber-local cache to avoid
110
+ # leaking pooled connections between tests.
111
+ @client.shutdown
112
+ Fiber[:workos_connections] = nil
113
+ end
114
+
89
115
  def test_request_dispatches_known_methods
90
116
  client = RecordingClient.new(api_key: "sk_test_123")
91
117
 
@@ -170,4 +196,141 @@ class BaseClientTest < Minitest::Test
170
196
  assert evict.finished
171
197
  refute keep.finished
172
198
  end
199
+
200
+ # An exception execute_request's rescue clause doesn't list still leaves the
201
+ # socket mid-stream, so the connection must not survive in the pool for the
202
+ # next request on this thread to pick up.
203
+ def test_unlisted_request_error_evicts_the_pooled_connection
204
+ conn = StubConnection.new(error: Net::HTTPBadResponse.new("wrong version"))
205
+ cache = @client.send(:thread_connections)
206
+ cache["https:api.workos.com:443:30"] = conn
207
+
208
+ assert_raises(Net::HTTPBadResponse) do
209
+ @client.execute_request(request: Net::HTTP::Get.new("/things"))
210
+ end
211
+
212
+ refute cache.key?("https:api.workos.com:443:30"),
213
+ "a connection whose request did not complete must not stay pooled"
214
+ assert conn.finished
215
+ end
216
+
217
+ def test_completed_request_keeps_the_connection_pooled
218
+ conn = StubConnection.new(response: Net::HTTPOK.new("1.1", "200", "OK"))
219
+ cache = @client.send(:thread_connections)
220
+ cache["https:api.workos.com:443:30"] = conn
221
+
222
+ @client.execute_request(request: Net::HTTP::Get.new("/things"))
223
+
224
+ assert cache.key?("https:api.workos.com:443:30")
225
+ refute conn.finished
226
+ end
227
+
228
+ # Raised asynchronously into the worker thread to stand in for a caller-side
229
+ # abort: Timeout.timeout, Thread#kill, a signal handler unwinding the stack.
230
+ # It descends from Exception rather than StandardError so that nothing in
231
+ # execute_request can catch it — the `ensure` is the only cleanup that runs.
232
+ class AbortSignal < Exception; end # standard:disable Lint/InheritException
233
+
234
+ # End-to-end regression test over a real keep-alive socket, for the failure
235
+ # the eviction actually prevents: request one is abandoned mid-flight, the
236
+ # server then writes response one onto that socket, and request two on the
237
+ # same thread reads those stale bytes as if they were its own response.
238
+ #
239
+ # Before the fix, request two sees marker "one" (or a mangled response) off
240
+ # the pooled socket. After it, the abandoned socket is closed and the server
241
+ # accepts a fresh connection for request two.
242
+ def test_aborted_request_does_not_leak_its_response_to_the_next_request
243
+ WebMock.disable!
244
+ server = TCPServer.new("127.0.0.1", 0)
245
+ port = server.addr[1]
246
+ client = WorkOS::BaseClient.new(
247
+ api_key: "sk_test_123",
248
+ base_url: "http://127.0.0.1:#{port}",
249
+ timeout: 5,
250
+ max_retries: 0
251
+ )
252
+
253
+ request_one_read = Queue.new
254
+ release_response_one = Queue.new
255
+ response_one_written = Queue.new
256
+ aborted = Queue.new
257
+ connections = Queue.new
258
+
259
+ server_thread = Thread.new do
260
+ # Connection one: read the request, then hold the response back until
261
+ # the client has been aborted and has unwound.
262
+ first = server.accept
263
+ connections << first
264
+ read_http_request(first)
265
+ request_one_read << true
266
+ release_response_one.pop
267
+ begin
268
+ write_http_response(first, {marker: "one"})
269
+ rescue Errno::EPIPE, Errno::ECONNRESET, IOError
270
+ # Expected once the fix closes the abandoned socket.
271
+ end
272
+ response_one_written << true
273
+
274
+ # Connection two: a fresh accept, which only completes because the
275
+ # client did not reuse the socket above.
276
+ second = server.accept
277
+ connections << second
278
+ read_http_request(second)
279
+ write_http_response(second, {marker: "two"})
280
+ end
281
+
282
+ worker = Thread.new do
283
+ begin
284
+ client.execute_request(request: Net::HTTP::Get.new("/one"))
285
+ rescue AbortSignal
286
+ # The caller unwinds but the thread survives and goes on to serve more
287
+ # work, the way a Puma or Solid Queue worker does. execute_request's
288
+ # `ensure` has already run by the time this body executes.
289
+ aborted << true
290
+ response_one_written.pop
291
+ end
292
+ response = client.execute_request(request: Net::HTTP::Get.new("/two"))
293
+ JSON.parse(response.body)["marker"]
294
+ end
295
+
296
+ marker = Timeout.timeout(15) do
297
+ request_one_read.pop
298
+ worker.raise(AbortSignal)
299
+ aborted.pop
300
+ release_response_one << true
301
+ worker.value
302
+ end
303
+
304
+ assert_equal "two", marker,
305
+ "request two read the abandoned socket's response instead of its own"
306
+ ensure
307
+ server_thread&.kill
308
+ worker&.kill
309
+ until connections.nil? || connections.empty?
310
+ socket = connections.pop
311
+ socket.close unless socket.closed?
312
+ end
313
+ server&.close
314
+ WebMock.enable!
315
+ end
316
+
317
+ def read_http_request(socket)
318
+ socket.gets # request line
319
+ loop do
320
+ line = socket.gets
321
+ break if line.nil? || line == "\r\n"
322
+ end
323
+ end
324
+
325
+ def write_http_response(socket, body)
326
+ payload = JSON.generate(body)
327
+ socket.write(
328
+ "HTTP/1.1 200 OK\r\n" \
329
+ "Content-Type: application/json\r\n" \
330
+ "Content-Length: #{payload.bytesize}\r\n" \
331
+ "Connection: keep-alive\r\n" \
332
+ "\r\n#{payload}"
333
+ )
334
+ socket.flush
335
+ end
173
336
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workos
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.2
4
+ version: 7.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - WorkOS
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-06 00:00:00.000000000 Z
11
+ date: 2026-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt