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 +4 -4
- data/.github/workflows/release-please.yml +3 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +2 -2
- data/lib/workos/base_client.rb +14 -1
- data/lib/workos/version.rb +1 -1
- data/test/workos/test_base_client.rb +155 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c34fe37d22956705fc72fc7bc0aabd35e5dde5a4a784263705ac684708d8482
|
|
4
|
+
data.tar.gz: 8d40b87964c3fb62b7f06bf58d9a93256a37aa45428741ba61f8f05fa0b93aba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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
|
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.
|
|
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.
|
|
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
|
data/lib/workos/base_client.rb
CHANGED
|
@@ -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
|
-
|
|
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)
|
data/lib/workos/version.rb
CHANGED
|
@@ -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
|