Skip to content

Commit d1e6836

Browse files
authored
Replace URLConnection with HttpClient (#6188)
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
1 parent 659ece5 commit d1e6836

File tree

6 files changed

+75
-52
lines changed

6 files changed

+75
-52
lines changed

modules/nextflow/src/main/groovy/nextflow/scm/AzureRepositoryProvider.groovy

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package nextflow.scm
1818

19+
import java.net.http.HttpResponse
1920
import java.util.regex.Pattern
2021

2122
import groovy.transform.CompileDynamic
@@ -162,14 +163,11 @@ final class AzureRepositoryProvider extends RepositoryProvider {
162163
*
163164
* @param connection A {@link HttpURLConnection} connection instance
164165
*/
165-
protected checkResponse( HttpURLConnection connection ) {
166-
167-
if (connection.getHeaderFields().containsKey("x-ms-continuationtoken")) {
168-
this.continuationToken = connection.getHeaderField("x-ms-continuationtoken");
169-
} else {
170-
this.continuationToken = null
171-
}
172-
166+
protected checkResponse( HttpResponse<String> connection ) {
167+
this.continuationToken = connection
168+
.headers()
169+
.firstValue("x-ms-continuationtoken")
170+
.orElse(null)
173171
super.checkResponse(connection)
174172
}
175173

modules/nextflow/src/main/groovy/nextflow/scm/GiteaRepositoryProvider.groovy

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
package nextflow.scm
1818

19+
1920
import groovy.transform.CompileDynamic
2021
import groovy.transform.CompileStatic
21-
2222
/**
2323
* Implements a repository provider for Gitea service
2424
*
@@ -43,14 +43,14 @@ final class GiteaRepositoryProvider extends RepositoryProvider {
4343
}
4444

4545
@Override
46-
protected void auth( URLConnection connection ) {
46+
protected String[] getAuth() {
4747
if( config.token ) {
4848
// set the token in the request header
4949
// https://docs.gitea.io/en-us/api-usage/#authentication
50-
connection.setRequestProperty("Authorization", "token $config.token")
50+
return new String[] { "Authorization", "token $config.token" as String }
5151
}
5252
else {
53-
super.auth(connection)
53+
return EMPTY_ARRAY
5454
}
5555
}
5656

modules/nextflow/src/main/groovy/nextflow/scm/GitlabRepositoryProvider.groovy

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ class GitlabRepositoryProvider extends RepositoryProvider {
3939
}
4040

4141
@Override
42-
protected void auth( URLConnection connection ) {
42+
protected String[] getAuth() {
4343
if( config.token ) {
4444
// set the token in the request header
45-
connection.setRequestProperty("PRIVATE-TOKEN", config.token)
46-
} else if( config.password ) {
47-
connection.setRequestProperty("PRIVATE-TOKEN", config.password)
45+
return new String[] { "PRIVATE-TOKEN", config.token }
4846
}
47+
if( config.password ) {
48+
return new String[] { "PRIVATE-TOKEN", config.password }
49+
}
50+
return EMPTY_ARRAY
4951
}
5052

5153
@Override

modules/nextflow/src/main/groovy/nextflow/scm/RepositoryProvider.groovy

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ package nextflow.scm
1818

1919
import static nextflow.util.StringUtils.*
2020

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+
2127
import groovy.json.JsonSlurper
2228
import groovy.transform.Canonical
2329
import groovy.transform.CompileStatic
@@ -26,6 +32,7 @@ import groovy.util.logging.Slf4j
2632
import nextflow.Const
2733
import nextflow.exception.AbortOperationException
2834
import nextflow.exception.RateLimitExceededException
35+
import nextflow.util.Threads
2936
import org.eclipse.jgit.api.Git
3037
import org.eclipse.jgit.lib.Ref
3138
import org.eclipse.jgit.transport.CredentialsProvider
@@ -40,6 +47,8 @@ import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
4047
@CompileStatic
4148
abstract class RepositoryProvider {
4249

50+
static final public String[] EMPTY_ARRAY = new String[0]
51+
4352
@Canonical
4453
static class TagInfo {
4554
String name
@@ -52,6 +61,11 @@ abstract class RepositoryProvider {
5261
String commitId
5362
}
5463

64+
/**
65+
* The client used to carry out http requests
66+
*/
67+
private HttpClient httpClient = { newHttpClient() }()
68+
5569
/**
5670
* The pipeline qualified name following the syntax {@code owner/repository}
5771
*/
@@ -184,24 +198,17 @@ abstract class RepositoryProvider {
184198
assert api
185199

186200
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()
205212
}
206213

207214
protected String getAuthObfuscated() {
@@ -211,15 +218,20 @@ abstract class RepositoryProvider {
211218
}
212219

213220
/**
214-
* Sets the authentication credential on the connection object
221+
* Define the credentials to be used to authenticate the http request
215222
*
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.
217228
*/
218-
protected void auth( URLConnection connection ) {
229+
protected String[] getAuth() {
219230
if( hasCredentials() ) {
220231
String authString = "${getUser()}:${getPassword()}".bytes.encodeBase64().toString()
221-
connection.setRequestProperty("Authorization","Basic " + authString)
232+
return new String[] { "Authorization", "Basic " + authString }
222233
}
234+
return EMPTY_ARRAY
223235
}
224236

225237
/**
@@ -228,17 +240,17 @@ abstract class RepositoryProvider {
228240
*
229241
* @param connection A {@link HttpURLConnection} connection instance
230242
*/
231-
protected checkResponse( HttpURLConnection connection ) {
232-
def code = connection.getResponseCode()
243+
protected checkResponse( HttpResponse<String> connection ) {
244+
final code = connection.statusCode()
233245

234246
switch( code ) {
235247
case 401:
236-
log.debug "Response status: $code -- ${connection.getErrorStream()?.text}"
248+
log.debug "Response status: $code -- ${connection.body()}"
237249
throw new AbortOperationException("Not authorized -- Check that the ${name.capitalize()} user name and password provided are correct")
238250

239251
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)
242254
if( limit == '0' ) {
243255
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"
244256
throw new RateLimitExceededException("API rate limit exceeded -- $message")
@@ -248,8 +260,8 @@ abstract class RepositoryProvider {
248260
throw new AbortOperationException("Forbidden -- $message")
249261
}
250262
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()}")
253265
}
254266
}
255267

@@ -342,4 +354,15 @@ abstract class RepositoryProvider {
342354
}
343355
}
344356

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+
345368
}

modules/nextflow/src/test/groovy/nextflow/scm/GithubRepositoryProviderTest.groovy

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,15 @@ class GithubRepositoryProviderTest extends Specification {
126126
SysEnv.push(['GITHUB_TOKEN': '1234567890'])
127127
and:
128128
def provider = Spy(new GithubRepositoryProvider('foo',Mock(ProviderConfig)))
129-
and:
130-
def conn = Mock(HttpURLConnection)
131129

132130
when:
133-
provider.auth(conn)
131+
final result = provider.getAuth()
134132
then:
135133
_ * provider.getUser()
136134
_ * provider.getPassword()
137135
1 * provider.hasCredentials()
138136
and:
139-
1 * conn.setRequestProperty('Authorization', "Basic ${'1234567890:x-oauth-basic'.bytes.encodeBase64()}".toString())
137+
result == new String[] { 'Authorization', "Basic ${'1234567890:x-oauth-basic'.bytes.encodeBase64()}".toString() }
140138

141139
cleanup:
142140
SysEnv.pop()

modules/nextflow/src/test/groovy/nextflow/scm/RepositoryProviderTest.groovy

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,21 @@ class RepositoryProviderTest extends Specification {
9393
def conn = Mock(HttpURLConnection)
9494

9595
when:
96-
provider.auth(conn)
96+
def headers = provider.getAuth()
9797
then:
9898
1 * provider.getUser() >> null
9999
1 * provider.hasCredentials()
100100
0 * conn.setRequestProperty('Authorization', _)
101+
and:
102+
headers == [] as String[]
101103

102104
when:
103-
provider.auth(conn)
105+
headers = provider.getAuth()
104106
then:
105107
_ * provider.getUser() >> 'foo'
106108
_ * provider.getPassword() >> 'bar'
107109
1 * provider.hasCredentials()
108110
and:
109-
1 * conn.setRequestProperty('Authorization', "Basic ${'foo:bar'.bytes.encodeBase64()}")
111+
headers == new String[] { 'Authorization', "Basic ${'foo:bar'.bytes.encodeBase64()}" }
110112
}
111113
}

0 commit comments

Comments
 (0)