Skip to content

Commit e0ff1e2

Browse files
authored
Treat HTTP 304 as idempotent success for container start/stop (#13)
* Treat non-redirect 304 responses as API status errors * Treat Docker 304 as success for container start/stop
1 parent 2c9b575 commit e0ff1e2

3 files changed

Lines changed: 50 additions & 3 deletions

File tree

lib/docker_engine_ruby/internal/transport/base_client.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class BaseClient
1111

1212
# from whatwg fetch spec
1313
MAX_REDIRECTS = 20
14+
REDIRECT_STATUSES = [301, 302, 303, 307, 308].freeze
1415

1516
# rubocop:disable Style/MutableConstant
1617
PLATFORM_HEADERS =
@@ -67,6 +68,15 @@ def should_retry?(status, headers:)
6768
end
6869
end
6970

71+
# @api private
72+
#
73+
# @param status [Integer]
74+
#
75+
# @return [Boolean]
76+
def redirect_status?(status)
77+
REDIRECT_STATUSES.include?(status)
78+
end
79+
7080
# @api private
7181
#
7282
# @param request [Hash{Symbol=>Object}] .
@@ -383,7 +393,8 @@ def send_request(request, redirect_count:, retry_count:, send_retry_header:)
383393
case status
384394
in ..299
385395
[status, response, stream]
386-
in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
396+
in Integer => status if self.class.redirect_status?(status) &&
397+
redirect_count >= self.class::MAX_REDIRECTS
387398
self.class.reap_connection!(status, stream: stream)
388399

389400
message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
@@ -392,7 +403,7 @@ def send_request(request, redirect_count:, retry_count:, send_retry_header:)
392403
response: response,
393404
message: message
394405
)
395-
in 300..399
406+
in Integer => status if self.class.redirect_status?(status)
396407
self.class.reap_connection!(status, stream: stream)
397408

398409
request = self.class.follow_redirect(request, status: status, response_headers: headers)
@@ -404,7 +415,7 @@ def send_request(request, redirect_count:, retry_count:, send_retry_header:)
404415
)
405416
in DockerEngineRuby::Errors::APIConnectionError if retry_count >= max_retries
406417
raise status
407-
in (400..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: headers)
418+
in (300..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: headers)
408419
decoded = Kernel.then do
409420
DockerEngineRuby::Internal::Util.decode_content(headers, stream: stream, suppress_error: true)
410421
ensure

lib/docker_engine_ruby/resources/containers.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,9 @@ def start(id, params = {})
483483
model: NilClass,
484484
options: options
485485
)
486+
rescue DockerEngineRuby::Errors::APIStatusError => e
487+
raise e unless e.status == 304
488+
nil
486489
end
487490

488491
# Get container stats based on resource usage
@@ -529,6 +532,9 @@ def stop(id, params = {})
529532
model: NilClass,
530533
options: options
531534
)
535+
rescue DockerEngineRuby::Errors::APIStatusError => e
536+
raise e unless e.status == 304
537+
nil
532538
end
533539

534540
# List processes running inside a container

test/docker_engine_ruby/client_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,36 @@ def test_images_pull_with_json_lines_response_returns_nil
296296
assert_nil(response)
297297
end
298298

299+
def test_containers_start_304_is_treated_as_success
300+
stub_request(:post, "http://localhost/containers/id/start").to_return(status: 304, body: "")
301+
302+
docker = DockerEngineRuby::Client.new(base_url: "http://localhost")
303+
304+
response = docker.containers.start("id")
305+
306+
assert_nil(response)
307+
end
308+
309+
def test_containers_stop_304_is_treated_as_success
310+
stub_request(:post, "http://localhost/containers/id/stop").to_return(status: 304, body: "")
311+
312+
docker = DockerEngineRuby::Client.new(base_url: "http://localhost")
313+
314+
response = docker.containers.stop("id")
315+
316+
assert_nil(response)
317+
end
318+
319+
def test_non_redirect_304_is_treated_as_status_error_for_other_operations
320+
stub_request(:get, "http://localhost/containers/json").to_return(status: 304, body: "")
321+
322+
docker = DockerEngineRuby::Client.new(base_url: "http://localhost")
323+
324+
error = assert_raises(DockerEngineRuby::Errors::APIStatusError) { docker.containers.list }
325+
326+
assert_equal(304, error.status)
327+
end
328+
299329
def test_default_headers
300330
stub_request(:get, "http://localhost/containers/json").to_return_json(status: 200, body: {})
301331

0 commit comments

Comments
 (0)