@@ -18,6 +18,12 @@ package nextflow.scm
18
18
19
19
import static nextflow.util.StringUtils.*
20
20
21
+ import java.net.http.HttpClient
22
+ import java.net.http.HttpRequest
23
+ import java.net.http.HttpResponse
24
+ import java.time.Duration
25
+ import java.util.concurrent.Executors
26
+
21
27
import groovy.json.JsonSlurper
22
28
import groovy.transform.Canonical
23
29
import groovy.transform.CompileStatic
@@ -26,6 +32,7 @@ import groovy.util.logging.Slf4j
26
32
import nextflow.Const
27
33
import nextflow.exception.AbortOperationException
28
34
import nextflow.exception.RateLimitExceededException
35
+ import nextflow.util.Threads
29
36
import org.eclipse.jgit.api.Git
30
37
import org.eclipse.jgit.lib.Ref
31
38
import org.eclipse.jgit.transport.CredentialsProvider
@@ -40,6 +47,8 @@ import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
40
47
@CompileStatic
41
48
abstract class RepositoryProvider {
42
49
50
+ static final public String [] EMPTY_ARRAY = new String [0 ]
51
+
43
52
@Canonical
44
53
static class TagInfo {
45
54
String name
@@ -52,6 +61,11 @@ abstract class RepositoryProvider {
52
61
String commitId
53
62
}
54
63
64
+ /**
65
+ * The client used to carry out http requests
66
+ */
67
+ private HttpClient httpClient = { newHttpClient() }()
68
+
55
69
/**
56
70
* The pipeline qualified name following the syntax {@code owner/repository }
57
71
*/
@@ -184,24 +198,17 @@ abstract class RepositoryProvider {
184
198
assert api
185
199
186
200
log. debug " Request [credentials ${ getAuthObfuscated() ?: '-'} ] -> $api "
187
- def connection = new URL (api). openConnection() as URLConnection
188
- connection. setConnectTimeout(60_000)
189
-
190
- auth(connection)
191
-
192
- if ( connection instanceof HttpURLConnection ) {
193
- checkResponse(connection)
194
- }
195
-
196
- InputStream content = connection. getInputStream()
197
- try {
198
- final result = content. text
199
- log. trace " Git provider HTTP request: '$api ' -- Response:\n ${ result} "
200
- return result
201
- }
202
- finally {
203
- content?. close()
204
- }
201
+ final request = HttpRequest
202
+ .newBuilder()
203
+ .uri(new URI (api))
204
+ .headers(getAuth())
205
+ .GET ()
206
+ // submit the request
207
+ final resp = httpClient. send(request. build(), HttpResponse.BodyHandlers . ofString())
208
+ // check the response code
209
+ checkResponse(resp)
210
+ // return the body as string
211
+ return resp. body()
205
212
}
206
213
207
214
protected String getAuthObfuscated () {
@@ -211,15 +218,20 @@ abstract class RepositoryProvider {
211
218
}
212
219
213
220
/**
214
- * Sets the authentication credential on the connection object
221
+ * Define the credentials to be used to authenticate the http request
215
222
*
216
- * @param connection The URL connection object to be authenticated
223
+ * @return
224
+ * A string array holding the authentication HTTP headers e.g.
225
+ * {@code [ "Authorization", "Bearer 1234567890"] } or an empty
226
+ * array when the credentials are not available or provided.
227
+ * Note: {@code null } is not a valid return value for this method.
217
228
*/
218
- protected void auth ( URLConnection connection ) {
229
+ protected String [] getAuth ( ) {
219
230
if ( hasCredentials() ) {
220
231
String authString = " ${ getUser()} :${ getPassword()} " . bytes. encodeBase64(). toString()
221
- connection . setRequestProperty( " Authorization" ," Basic " + authString)
232
+ return new String [] { " Authorization" , " Basic " + authString }
222
233
}
234
+ return EMPTY_ARRAY
223
235
}
224
236
225
237
/**
@@ -228,17 +240,17 @@ abstract class RepositoryProvider {
228
240
*
229
241
* @param connection A {@link HttpURLConnection} connection instance
230
242
*/
231
- protected checkResponse ( HttpURLConnection connection ) {
232
- def code = connection. getResponseCode ()
243
+ protected checkResponse ( HttpResponse< String > connection ) {
244
+ final code = connection. statusCode ()
233
245
234
246
switch ( code ) {
235
247
case 401 :
236
- log. debug " Response status: $code -- ${ connection.getErrorStream()?.text } "
248
+ log. debug " Response status: $code -- ${ connection.body() } "
237
249
throw new AbortOperationException (" Not authorized -- Check that the ${ name.capitalize()} user name and password provided are correct" )
238
250
239
251
case 403 :
240
- log. debug " Response status: $code -- ${ connection.getErrorStream()?.text } "
241
- def limit = connection. getHeaderField( ' X-RateLimit-Remaining' )
252
+ log. debug " Response status: $code -- ${ connection.body() } "
253
+ def limit = connection. headers() . firstValue( ' X-RateLimit-Remaining' ) . orElse( null )
242
254
if ( limit == ' 0' ) {
243
255
def message = config. auth ? " Check ${ name.capitalize()} 's API rate limits for more details" : " Provide your ${ name.capitalize()} user name and password to get a higher rate limit"
244
256
throw new RateLimitExceededException (" API rate limit exceeded -- $message " )
@@ -248,8 +260,8 @@ abstract class RepositoryProvider {
248
260
throw new AbortOperationException (" Forbidden -- $message " )
249
261
}
250
262
case 404 :
251
- log. debug " Response status: $code -- ${ connection.getErrorStream()?.text } "
252
- throw new AbortOperationException (" Remote resource not found: ${ connection.getURL ()} " )
263
+ log. debug " Response status: $code -- ${ connection.body() } "
264
+ throw new AbortOperationException (" Remote resource not found: ${ connection.uri ()} " )
253
265
}
254
266
}
255
267
@@ -342,4 +354,15 @@ abstract class RepositoryProvider {
342
354
}
343
355
}
344
356
357
+ private HttpClient newHttpClient () {
358
+ final builder = HttpClient . newBuilder()
359
+ .version(HttpClient.Version . HTTP_1_1 )
360
+ .connectTimeout(Duration . ofSeconds(60 ))
361
+ // use virtual threads executor if enabled
362
+ if ( Threads . useVirtual() )
363
+ builder. executor(Executors . newVirtualThreadPerTaskExecutor())
364
+ // build and return the new client
365
+ return builder. build()
366
+ }
367
+
345
368
}
0 commit comments