workos 10.2.0 → 10.2.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: 63f6d02af5ecf1ab9f62af255a77ef281f8763b3e6c56feeb6ee87da3817c29d
4
- data.tar.gz: 1691eb9636159a73295a6baabf6b63103bfc868f4f7660e49ae12644d7f27efa
3
+ metadata.gz: b7aff393f8f0d21fe3ff062ac8a0f835ad3d2f43bcfc300d55ec9f5a19688774
4
+ data.tar.gz: b94cd0e24ce40d7e61bc0c9d3578f14b1c627a4ebf935fdbad7d295584c75dec
5
5
  SHA512:
6
- metadata.gz: 7b421f5433d3001c3dcb8de0aeddf93805bcbddd73c1a9c475b65f6c66b4e1a442d2a6eca5c37444f9dc97481abf5c7f05799bd914431de5411f368566e85a99
7
- data.tar.gz: c6376c12ff9efe67ac13aaf63e63a3c5ddcad6926272b0cc1c36cde0ac4ed858c10157b3ee064143152a73c28674d848ecd81f200a2f64b1e7ae2a23def4c44a
6
+ metadata.gz: 235bb3273c2b574dad6cd967b69d5b832cc0bbfbcb7f8389e0eb53e909f915a6698047f68abf7e6826196e7e62a94a43c73686207236439a44c608198be374eb
7
+ data.tar.gz: bdb237a96780cac60c5bbe0ab7f0bc41bea1d31f38928111ef7a470f3d0e21d7b15e35b6190b7f0588f63dd707d442846293630260c3cd29b8ccf2d9dee7dac6
@@ -8,6 +8,6 @@ on:
8
8
 
9
9
  jobs:
10
10
  block-generated-edits:
11
- uses: workos/actions/.github/workflows/block-generated-edits.yml@main
11
+ uses: workos/actions/.github/workflows/block-generated-edits.yml@5c7450a04f5a182b346eae2d833b869e337b12bb # main
12
12
  secrets:
13
13
  SDK_BOT_PRIVATE_KEY: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
@@ -21,8 +21,8 @@ jobs:
21
21
  - '3.4'
22
22
  - '4.0'
23
23
  steps:
24
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3
25
- - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # 1.316.0
24
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # 6.1.0
25
+ - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # 1.321.0
26
26
  with:
27
27
  ruby-version: ${{ matrix.ruby }}
28
28
  bundler-cache: true
@@ -14,8 +14,8 @@ jobs:
14
14
  permissions:
15
15
  contents: read
16
16
  steps:
17
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3
18
- - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # 1.316.0
17
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # 6.1.0
18
+ - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # 1.321.0
19
19
  with:
20
20
  bundler-cache: true
21
21
  - run: ./script/docs
@@ -15,8 +15,8 @@ jobs:
15
15
  name: Lint
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3
19
- - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # 1.316.0
18
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # 6.1.0
19
+ - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # 1.321.0
20
20
  with:
21
21
  bundler-cache: true
22
22
 
@@ -61,7 +61,7 @@ jobs:
61
61
  permission-pull-requests: write
62
62
 
63
63
  - name: Checkout release PR branch
64
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3
64
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # 6.1.0
65
65
  with:
66
66
  ref: ${{ fromJSON(needs.release-please.outputs.pr).headBranchName }}
67
67
  token: ${{ steps.generate-token.outputs.token }}
@@ -155,7 +155,7 @@ jobs:
155
155
  git push
156
156
 
157
157
  - name: Set up Ruby
158
- uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # 1.316.0
158
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # 1.321.0
159
159
  with:
160
160
  ruby-version: ruby
161
161
 
@@ -190,7 +190,7 @@ jobs:
190
190
  permission-contents: write
191
191
  permission-pull-requests: write
192
192
 
193
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3
193
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # 6.1.0
194
194
  with:
195
195
  token: ${{ steps.generate-token.outputs.token }}
196
196
 
@@ -22,10 +22,10 @@ jobs:
22
22
  role-to-assume: rg_oidc_akr_fn8dx45asckvmsnd2kka
23
23
 
24
24
  - name: Checkout
25
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3
25
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # 6.1.0
26
26
 
27
27
  - name: Setup Ruby
28
- uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # 1.316.0
28
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # 1.321.0
29
29
  with:
30
30
  bundler-cache: true
31
31
 
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "10.2.0"
2
+ ".": "10.2.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [10.2.1](https://github.com/workos/workos-ruby/compare/v10.2.0...v10.2.1) (2026-08-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * explicitly evict connections if request does not complete as success ([e792414](https://github.com/workos/workos-ruby/commit/e79241473e9cb0e8898632723200a7e23682ff24))
9
+
3
10
  ## [10.2.0](https://github.com/workos/workos-ruby/compare/v10.1.0...v10.2.0) (2026-08-11)
4
11
 
5
12
  * [#533](https://github.com/workos/workos-ruby/pull/533) feat(generated)!: regenerate from spec (2 changes)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- workos (10.2.0)
4
+ workos (10.2.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 (10.2.0)
156
+ workos (10.2.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 = "10.2.0"
5
+ VERSION = "10.2.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
@@ -206,6 +224,143 @@ class BaseClientTest < Minitest::Test
206
224
  refute keep.finished
207
225
  end
208
226
 
227
+ # An exception execute_request's rescue clause doesn't list still leaves the
228
+ # socket mid-stream, so the connection must not survive in the pool for the
229
+ # next request on this thread to pick up.
230
+ def test_unlisted_request_error_evicts_the_pooled_connection
231
+ conn = StubConnection.new(error: Net::HTTPBadResponse.new("wrong version"))
232
+ cache = @client.send(:thread_connections)
233
+ cache["https:api.workos.com:443:30"] = conn
234
+
235
+ assert_raises(Net::HTTPBadResponse) do
236
+ @client.execute_request(request: Net::HTTP::Get.new("/things"))
237
+ end
238
+
239
+ refute cache.key?("https:api.workos.com:443:30"),
240
+ "a connection whose request did not complete must not stay pooled"
241
+ assert conn.finished
242
+ end
243
+
244
+ def test_completed_request_keeps_the_connection_pooled
245
+ conn = StubConnection.new(response: Net::HTTPOK.new("1.1", "200", "OK"))
246
+ cache = @client.send(:thread_connections)
247
+ cache["https:api.workos.com:443:30"] = conn
248
+
249
+ @client.execute_request(request: Net::HTTP::Get.new("/things"))
250
+
251
+ assert cache.key?("https:api.workos.com:443:30")
252
+ refute conn.finished
253
+ end
254
+
255
+ # Raised asynchronously into the worker thread to stand in for a caller-side
256
+ # abort: Timeout.timeout, Thread#kill, a signal handler unwinding the stack.
257
+ # It descends from Exception rather than StandardError so that nothing in
258
+ # execute_request can catch it — the `ensure` is the only cleanup that runs.
259
+ class AbortSignal < Exception; end # standard:disable Lint/InheritException
260
+
261
+ # End-to-end regression test over a real keep-alive socket, for the failure
262
+ # the eviction actually prevents: request one is abandoned mid-flight, the
263
+ # server then writes response one onto that socket, and request two on the
264
+ # same thread reads those stale bytes as if they were its own response.
265
+ #
266
+ # Before the fix, request two sees marker "one" (or a mangled response) off
267
+ # the pooled socket. After it, the abandoned socket is closed and the server
268
+ # accepts a fresh connection for request two.
269
+ def test_aborted_request_does_not_leak_its_response_to_the_next_request
270
+ WebMock.disable!
271
+ server = TCPServer.new("127.0.0.1", 0)
272
+ port = server.addr[1]
273
+ client = WorkOS::BaseClient.new(
274
+ api_key: "sk_test_123",
275
+ base_url: "http://127.0.0.1:#{port}",
276
+ timeout: 5,
277
+ max_retries: 0
278
+ )
279
+
280
+ request_one_read = Queue.new
281
+ release_response_one = Queue.new
282
+ response_one_written = Queue.new
283
+ aborted = Queue.new
284
+ connections = Queue.new
285
+
286
+ server_thread = Thread.new do
287
+ # Connection one: read the request, then hold the response back until
288
+ # the client has been aborted and has unwound.
289
+ first = server.accept
290
+ connections << first
291
+ read_http_request(first)
292
+ request_one_read << true
293
+ release_response_one.pop
294
+ begin
295
+ write_http_response(first, {marker: "one"})
296
+ rescue Errno::EPIPE, Errno::ECONNRESET, IOError
297
+ # Expected once the fix closes the abandoned socket.
298
+ end
299
+ response_one_written << true
300
+
301
+ # Connection two: a fresh accept, which only completes because the
302
+ # client did not reuse the socket above.
303
+ second = server.accept
304
+ connections << second
305
+ read_http_request(second)
306
+ write_http_response(second, {marker: "two"})
307
+ end
308
+
309
+ worker = Thread.new do
310
+ begin
311
+ client.execute_request(request: Net::HTTP::Get.new("/one"))
312
+ rescue AbortSignal
313
+ # The caller unwinds but the thread survives and goes on to serve more
314
+ # work, the way a Puma or Solid Queue worker does. execute_request's
315
+ # `ensure` has already run by the time this body executes.
316
+ aborted << true
317
+ response_one_written.pop
318
+ end
319
+ response = client.execute_request(request: Net::HTTP::Get.new("/two"))
320
+ JSON.parse(response.body)["marker"]
321
+ end
322
+
323
+ marker = Timeout.timeout(15) do
324
+ request_one_read.pop
325
+ worker.raise(AbortSignal)
326
+ aborted.pop
327
+ release_response_one << true
328
+ worker.value
329
+ end
330
+
331
+ assert_equal "two", marker,
332
+ "request two read the abandoned socket's response instead of its own"
333
+ ensure
334
+ server_thread&.kill
335
+ worker&.kill
336
+ until connections.nil? || connections.empty?
337
+ socket = connections.pop
338
+ socket.close unless socket.closed?
339
+ end
340
+ server&.close
341
+ WebMock.enable!
342
+ end
343
+
344
+ def read_http_request(socket)
345
+ socket.gets # request line
346
+ loop do
347
+ line = socket.gets
348
+ break if line.nil? || line == "\r\n"
349
+ end
350
+ end
351
+
352
+ def write_http_response(socket, body)
353
+ payload = JSON.generate(body)
354
+ socket.write(
355
+ "HTTP/1.1 200 OK\r\n" \
356
+ "Content-Type: application/json\r\n" \
357
+ "Content-Length: #{payload.bytesize}\r\n" \
358
+ "Connection: keep-alive\r\n" \
359
+ "\r\n#{payload}"
360
+ )
361
+ socket.flush
362
+ end
363
+
209
364
  # Regression test for https://github.com/workos/workos-ruby/issues/496
210
365
  #
211
366
  # 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: 10.2.0
4
+ version: 10.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - WorkOS