Skip to content

Commit ad36919

Browse files
#253 fix node transfer list infinite loop with once_only.
Signed-off-by: Laurent Martin <laurent.martin.l@gmail.com>
1 parent 180e528 commit ad36919

4 files changed

Lines changed: 58 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Released: [Place date of release here]
1010

1111
### Issues Fixed
1212

13+
* `node`: #253 fix `node transfer list` infinite loop with `once_only`.
14+
1315
### Breaking Changes
1416

1517
## 4.25.5

lib/aspera/api/node.rb

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -530,32 +530,45 @@ def read_with_paging(subpath, query = nil, iteration: nil, **call_args)
530530
Aspera.assert_type(query, Hash, NilClass){'query'}
531531
Aspera.assert(!call_args.key?(:query))
532532
query = {} if query.nil?
533-
query[:iteration_token] = iteration[0] unless iteration.nil?
533+
query[:iteration_token] = iteration[0] unless iteration.nil? || iteration[0].nil?
534534
max = query.delete(RestList::MAX_ITEMS)
535+
# Return empty list immediately if max is 0
536+
return [] if max&.zero?
535537
item_list = []
536538
loop do
537539
data, http = read(subpath, query, **call_args, ret: :both)
538540
Aspera.assert_type(data, Array){"Expected data to be an Array, got: #{data.class}"}
539541
# no data
540542
break if data.empty?
541-
# get next iteration token from link
542-
next_iteration_token = nil
543-
link_info = http['Link']
544-
unless link_info.nil?
545-
m = link_info.match(/<([^>]+)>/)
546-
Aspera.assert(m){"Cannot parse iteration in Link: #{link_info}"}
547-
next_iteration_token = Rest.query_to_h(URI.parse(m[1]).query)['iteration_token']
548-
end
549-
# same as last iteration: stop
550-
break if next_iteration_token&.eql?(query[:iteration_token])
551-
query[:iteration_token] = next_iteration_token
552543
item_list.concat(data)
544+
# Check if we reached the max limit
553545
if max&.<=(item_list.length)
554546
item_list = item_list.slice(0, max)
555547
break
556548
end
549+
# Update progress spinner
550+
RestParameters.instance.spinner_cb.call(item_list.length)
551+
# Parse Link header according to RFC 8288 to extract next iteration token
552+
next_url = Rest.parse_link_header(http['Link'], rel: 'next')
553+
next_iteration_token = nil
554+
if next_url
555+
begin
556+
parsed_uri = URI.parse(next_url)
557+
query_params = Rest.query_to_h(parsed_uri.query) if parsed_uri.query
558+
next_iteration_token = query_params['iteration_token'] if query_params
559+
rescue URI::InvalidURIError => e
560+
Log.log.warn{"Invalid URI in Link header: #{next_url} - #{e.message}"}
561+
end
562+
end
563+
# Stop if no next token
557564
break if next_iteration_token.nil?
565+
# Stop if same token as current (infinite loop protection)
566+
break if next_iteration_token.eql?(query[:iteration_token])
567+
# Update token for next iteration
568+
query[:iteration_token] = next_iteration_token
558569
end
570+
# Signal completion
571+
RestParameters.instance.spinner_cb.call(action: :success)
559572
# save iteration token if needed
560573
iteration[0] = query[:iteration_token] unless iteration.nil?
561574
item_list

lib/aspera/rest.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,37 @@ def query_to_h(query)
150150
end
151151
end
152152

153+
# Parse Link header according to RFC 8288 to extract a specific relation
154+
# @param link_header [String, nil] The Link header value
155+
# @param rel [String] The relation to look for (default: 'next')
156+
# @return [String, nil] The URL of the link with the specified relation, or nil
157+
def parse_link_header(link_header, rel: 'next')
158+
return if link_header.nil? || link_header.empty?
159+
# RFC 8288: Link header format is: <URI>; param1=value1; param2=value2, <URI2>; ...
160+
# We look for the link with the specified rel
161+
link_header.split(',').each do |link_part|
162+
link_part = link_part.strip
163+
# Extract URL between < and >
164+
url_match = link_part.match(/<([^>]+)>/)
165+
next unless url_match
166+
url = url_match[1]
167+
# Extract parameters after the URL
168+
params_str = link_part[url_match.end(0)..-1]
169+
# Check if this link has the specified rel (with or without quotes, case insensitive)
170+
next unless /;\s*rel\s*=\s*"?#{Regexp.escape(rel)}"?/i.match?(params_str)
171+
return url
172+
end
173+
# Fallback: if no rel found and looking for 'next', try the first link (backward compatibility)
174+
if rel.eql?('next')
175+
first_link = link_header.split(',').first&.strip
176+
if first_link
177+
url_match = first_link.match(/<([^>]+)>/)
178+
return url_match[1] if url_match
179+
end
180+
end
181+
nil
182+
end
183+
153184
# Start a HTTP/S session, also used for web sockets
154185
# @param base_url [String] Base url of HTTP/S session
155186
# @return [Net::HTTP] A started HTTP session

tests/tests.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,7 +1250,6 @@ nd_xfer_lstb:
12501250
- --once-only=yes
12511251
nd_xfer_lst_once1:
12521252
tags:
1253-
- flaky
12541253
- nodoc
12551254
depends_on:
12561255
- nd_xfer_lstb
@@ -1261,7 +1260,6 @@ nd_xfer_lst_once1:
12611260
- --once-only=yes
12621261
nd_xfer_lst_once2:
12631262
tags:
1264-
- flaky
12651263
- nodoc
12661264
depends_on:
12671265
- nd_xfer_lst_once1

0 commit comments

Comments
 (0)