workos 9.5.0 → 9.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d298622f18f3a7fa937e4a37bb7e65ef37553a09ff6299fa32e5899f6ad62dfc
4
- data.tar.gz: c8ae0ea79b8d4d9713edc1c22f5f55afc47dc7f5c5374430f7ac3c1eb7c4138a
3
+ metadata.gz: 1c34fe37d22956705fc72fc7bc0aabd35e5dde5a4a784263705ac684708d8482
4
+ data.tar.gz: 8d40b87964c3fb62b7f06bf58d9a93256a37aa45428741ba61f8f05fa0b93aba
5
5
  SHA512:
6
- metadata.gz: e341612af96eb751ee97935c6560b80f647b7032bc37145fa953bf0ed3a72b432d947b8b93a95743ee6bad7bd8bb92336de4140dd202fc23e4ebc7c046dd53b4
7
- data.tar.gz: 8df40fc5b71007946287b91b0fedf215b2cb19312f6180c9ec577aecfdfbbfd06482228ba7519aa23eb0be065c5d55088073abcdbbcce65b2b161eb970d089ed
6
+ metadata.gz: 43edc59178986404b4ce4efbf2bfc614775dd53a82d987adea6e9713a38b71ab7b2c8c11e5579bdf26b83a48f980c493b2c387a2eb433c8dbc9293e4aa3edaf2
7
+ data.tar.gz: 3a302db04edaaf67e5b27e66a114587a7e9cef1d1ae163fad0e1b240207ac84faed2a60e7ebc8fabd62b0ea7ceb6ad84e64d1a0632830f6d87e7787c64e9fd20
@@ -3,10 +3,10 @@ name: Release Please
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - main
6
+ - v9.5.x
7
7
 
8
8
  concurrency:
9
- group: release-please
9
+ group: release-please-v9.5.x
10
10
  cancel-in-progress: false
11
11
 
12
12
  permissions:
@@ -40,6 +40,7 @@ jobs:
40
40
  id: release
41
41
  with:
42
42
  token: ${{ steps.generate-token.outputs.token }}
43
+ target-branch: v9.5.x
43
44
 
44
45
  # While the release PR is open, enrich it before it merges. Runs as its
45
46
  # own job (gated on the PR existing) so a failure here can never block the
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "9.5.0"
2
+ ".": "9.5.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [9.5.1](https://github.com/workos/workos-ruby/compare/v9.5.0...v9.5.1) (2026-08-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * explicitly evict connections if request does not complete as success ([bfee53e](https://github.com/workos/workos-ruby/commit/bfee53efbd80a2156cbd15b70b483be66913e714))
9
+
3
10
  ## [9.5.0](https://github.com/workos/workos-ruby/compare/v9.4.0...v9.5.0) (2026-07-06)
4
11
 
5
12
  * [#514](https://github.com/workos/workos-ruby/pull/514) fix(generated): regenerate from spec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- workos (9.5.0)
4
+ workos (9.5.1)
5
5
  jwt (~> 3.1)
6
6
  logger (~> 1.7)
7
7
  zeitwerk (~> 2.6)
@@ -153,7 +153,7 @@ CHECKSUMS
153
153
  unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
154
154
  webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
155
155
  webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
156
- workos (9.5.0)
156
+ workos (9.5.1)
157
157
  yard (0.9.44) sha256=eb087e9b631ccd887b049f303d489963945452d5e2a7eb49a5a74a7cf6887f28
158
158
  yard-markdown (0.7.1) sha256=06c378632dfe7ba053be9ba469eb4701aa0470e36bcf7e5546f353eb90c1bfd1
159
159
  zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
@@ -137,7 +137,20 @@ module WorkOS
137
137
  loggable_path = redact_path(request.path)
138
138
  log(:debug, "request start", method: request.method, path: loggable_path, attempt: attempt + 1)
139
139
  http = connection_for(base, timeout)
140
- response = http.request(request)
140
+ request_completed = false
141
+ begin
142
+ response = http.request(request)
143
+ request_completed = true
144
+ ensure
145
+ # Any exit from #request other than a returned response can leave
146
+ # the socket mid-stream: a connection error the rescue below knows
147
+ # about, one it doesn't (OpenSSL::SSL::SSLError,
148
+ # Net::HTTPBadResponse), or a non-local exit such as an
149
+ # application-level Timeout.timeout or Thread#kill. A half-read
150
+ # socket handed back to the pool desyncs the *next* request on this
151
+ # thread, so drop it here rather than in the rescue.
152
+ evict_connection(base) unless request_completed
153
+ end
141
154
  return response if response.is_a?(Net::HTTPSuccess)
142
155
 
143
156
  if attempt < retries && retryable?(response)
@@ -2,5 +2,5 @@
2
2
 
3
3
  # @oagen-ignore-file
4
4
  module WorkOS
5
- VERSION = "9.5.0"
5
+ VERSION = "9.5.1"
6
6
  end
@@ -82,6 +82,24 @@ 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
@@ -179,6 +197,143 @@ class BaseClientTest < Minitest::Test
179
197
  refute keep.finished
180
198
  end
181
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
336
+
182
337
  # Regression test for https://github.com/workos/workos-ruby/issues/496
183
338
  #
184
339
  # The connection cache must be isolated per thread. A previous version
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workos
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.5.0
4
+ version: 9.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - WorkOS