Skip to content

Commit 5793c53

Browse files
committed
[JENKINS-75157] Bitbucket client fail to retrieve resource with HTTP 404 for bitbucket server (#970)
Remove the HttpHost parameter when calling in Apache client as the request could be different than the host configured in jenkins, for example in Bitbucket Server request using mirror link. Do not catch the FileNotFoundException in executeMethod to respect the SCMFile interface.
1 parent de66cd2 commit 5793c53

File tree

6 files changed

+87
-52
lines changed

6 files changed

+87
-52
lines changed

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFile.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import edu.umd.cs.findbugs.annotations.NonNull;
2929
import java.io.IOException;
3030
import java.io.InputStream;
31+
import java.util.Collections;
3132
import jenkins.scm.api.SCMFile;
3233

3334
public class BitbucketSCMFile extends SCMFile {
@@ -45,13 +46,13 @@ public void setRef(String ref) {
4546
}
4647

4748
@Deprecated
48-
public BitbucketSCMFile(BitbucketSCMFileSystem bitBucketSCMFileSystem,
49+
public BitbucketSCMFile(BitbucketSCMFileSystem bitbucketSCMFileSystem,
4950
BitbucketApi api,
5051
String ref) {
51-
this(bitBucketSCMFileSystem, api, ref, null);
52+
this(bitbucketSCMFileSystem, api, ref, null);
5253
}
5354

54-
public BitbucketSCMFile(BitbucketSCMFileSystem bitBucketSCMFileSystem,
55+
public BitbucketSCMFile(BitbucketSCMFileSystem bitbucketSCMFileSystem,
5556
BitbucketApi api,
5657
String ref, String hash) {
5758
super();
@@ -85,7 +86,8 @@ public Iterable<SCMFile> children() throws IOException,
8586
if (this.isDirectory()) {
8687
return api.getDirectoryContent(this);
8788
} else {
88-
throw new IOException("Cannot get children from a regular file");
89+
// respect the interface javadoc
90+
return Collections.emptyList();
8991
}
9092
}
9193

@@ -108,7 +110,7 @@ public long lastModified() throws IOException, InterruptedException {
108110
@Override
109111
@NonNull
110112
protected SCMFile newChild(String name, boolean assumeIsDirectory) {
111-
return new BitbucketSCMFile(this, name, assumeIsDirectory?Type.DIRECTORY:Type.REGULAR_FILE, hash);
113+
return new BitbucketSCMFile(this, name, assumeIsDirectory ? Type.DIRECTORY : Type.REGULAR_FILE, hash);
112114
}
113115

114116
@Override

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
import hudson.Util;
4444
import hudson.model.Item;
4545
import hudson.model.Queue;
46-
import hudson.model.queue.Tasks;
4746
import hudson.scm.SCM;
4847
import hudson.scm.SCMDescriptor;
4948
import hudson.security.ACL;
@@ -116,22 +115,24 @@ public SCMFileSystem build(@NonNull Item owner, @NonNull SCM scm, @CheckForNull
116115
}
117116

118117
private static StandardCredentials lookupScanCredentials(@CheckForNull Item context,
119-
@CheckForNull String scanCredentialsId, String serverUrl) {
120-
if (Util.fixEmpty(scanCredentialsId) == null) {
118+
@CheckForNull String scanCredentialsId,
119+
String serverURL) {
120+
scanCredentialsId = Util.fixEmpty(scanCredentialsId);
121+
if (scanCredentialsId == null) {
121122
return null;
122123
} else {
123124
return CredentialsMatchers.firstOrNull(
124-
CredentialsProvider.lookupCredentials(
125+
CredentialsProvider.lookupCredentialsInItem(
125126
StandardCredentials.class,
126127
context,
127-
context instanceof Queue.Task
128-
? Tasks.getDefaultAuthenticationOf((Queue.Task) context)
129-
: ACL.SYSTEM,
130-
URIRequirementBuilder.fromUri(serverUrl).build()
128+
context instanceof Queue.Task task
129+
? task.getDefaultAuthentication2()
130+
: ACL.SYSTEM2,
131+
URIRequirementBuilder.fromUri(serverURL).build()
131132
),
132133
CredentialsMatchers.allOf(
133134
CredentialsMatchers.withId(scanCredentialsId),
134-
AuthenticationTokens.matcher(BitbucketAuthenticator.authenticationContext(serverUrl))
135+
AuthenticationTokens.matcher(BitbucketAuthenticator.authenticationContext(serverURL))
135136
)
136137
);
137138
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
3636
import java.io.InputStream;
3737
import java.net.InetSocketAddress;
3838
import java.net.Proxy;
39-
import java.net.URI;
40-
import java.net.URISyntaxException;
4139
import java.nio.charset.StandardCharsets;
4240
import java.util.List;
4341
import java.util.concurrent.TimeUnit;
@@ -220,21 +218,21 @@ private void setClientProxyParams(String host, HttpClientBuilder builder) {
220218
@NonNull
221219
protected abstract CloseableHttpClient getClient();
222220

223-
protected CloseableHttpResponse executeMethod(HttpHost host,
224-
HttpRequestBase httpMethod,
225-
boolean requireAuthentication) throws IOException {
221+
protected CloseableHttpResponse executeMethod(HttpRequestBase request, boolean requireAuthentication) throws IOException {
226222
if (requireAuthentication && authenticator != null) {
227-
authenticator.configureRequest(httpMethod);
223+
authenticator.configureRequest(request);
228224
}
229-
return getClient().execute(host, httpMethod, context);
225+
// the Apache client determinate the host from request.getURI()
226+
// in some cases like requests to mirror or avatar, the host could not be the same of configured in Jenkins
227+
return getClient().execute(request, context);
230228
}
231229

232-
protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws IOException {
233-
return executeMethod(host, httpMethod, true);
230+
protected CloseableHttpResponse executeMethod(HttpRequestBase httpMethod) throws IOException {
231+
return executeMethod(httpMethod, true);
234232
}
235233

236234
protected String doRequest(HttpRequestBase request, boolean requireAuthentication) throws IOException {
237-
try (CloseableHttpResponse response = executeMethod(getHost(), request, requireAuthentication)) {
235+
try (CloseableHttpResponse response = executeMethod(request, requireAuthentication)) {
238236
int statusCode = response.getStatusLine().getStatusCode();
239237
if (statusCode == HttpStatus.SC_NOT_FOUND) {
240238
throw new FileNotFoundException("URL: " + request.getURI());
@@ -250,7 +248,7 @@ protected String doRequest(HttpRequestBase request, boolean requireAuthenticatio
250248
throw buildResponseException(response, content);
251249
}
252250
return content;
253-
} catch (BitbucketRequestException e) {
251+
} catch (FileNotFoundException | BitbucketRequestException e) {
254252
throw e;
255253
} catch (IOException e) {
256254
throw new IOException("Communication error for url: " + request, e);
@@ -276,19 +274,7 @@ private void release(HttpRequestBase method) {
276274
*/
277275
protected InputStream getRequestAsInputStream(String path) throws IOException {
278276
HttpGet httpget = new HttpGet(path);
279-
HttpHost host = getHost();
280-
281-
// Extract host from URL, if present
282-
try {
283-
URI uri = new URI(host.toURI());
284-
if (uri.isAbsolute() && ! uri.isOpaque()) {
285-
host = HttpHost.create(uri.getScheme() + "://" + uri.getAuthority());
286-
}
287-
} catch (URISyntaxException ex) {
288-
// use default
289-
}
290-
291-
CloseableHttpResponse response = executeMethod(host, httpget);
277+
CloseableHttpResponse response = executeMethod(httpget);
292278
int statusCode = response.getStatusLine().getStatusCode();
293279
if (statusCode == HttpStatus.SC_NOT_FOUND) {
294280
EntityUtils.consume(response.getEntity());
@@ -303,7 +289,7 @@ protected InputStream getRequestAsInputStream(String path) throws IOException {
303289

304290
protected int headRequestStatus(String path) throws IOException {
305291
HttpHead httpHead = new HttpHead(path);
306-
try (CloseableHttpResponse response = executeMethod(getHost(), httpHead)) {
292+
try (CloseableHttpResponse response = executeMethod(httpHead)) {
307293
EntityUtils.consume(response.getEntity());
308294
return response.getStatusLine().getStatusCode();
309295
} catch (IOException e) {

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,7 +952,7 @@ protected CloseableHttpClient getClient() {
952952
protected HttpHost getHost() {
953953
String url = baseURL;
954954
try {
955-
// it's really needed?
955+
// it's needed because the serverURL can contains a context root different than '/' and the HttpHost must contains only schema, host and port
956956
URL tmp = new URL(baseURL);
957957
String schema = tmp.getProtocol() == null ? "http" : tmp.getProtocol();
958958
return new HttpHost(tmp.getHost(), tmp.getPort(), schema);

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
2828
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils;
2929
import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient;
30+
import java.io.FileNotFoundException;
3031
import java.io.IOException;
3132
import java.io.InputStream;
3233
import java.nio.charset.StandardCharsets;
3334
import org.apache.commons.io.IOUtils;
3435
import org.apache.http.HttpEntity;
35-
import org.apache.http.HttpHost;
3636
import org.apache.http.StatusLine;
3737
import org.apache.http.client.methods.CloseableHttpResponse;
3838
import org.apache.http.client.methods.HttpRequestBase;
@@ -53,7 +53,7 @@ default void request(HttpRequestBase request) {
5353
default CloseableHttpResponse loadResponseFromResources(Class<?> resourceBase, String path, String payloadPath) throws IOException {
5454
try (InputStream json = resourceBase.getResourceAsStream(payloadPath)) {
5555
if (json == null) {
56-
throw new IllegalStateException("Payload for the REST path " + path + " could not be found: " + payloadPath);
56+
throw new FileNotFoundException("Payload for the REST path " + path + " could not be found: " + payloadPath);
5757
}
5858
HttpEntity entity = mock(HttpEntity.class);
5959
String jsonString = IOUtils.toString(json, StandardCharsets.UTF_8);
@@ -104,19 +104,17 @@ private BitbucketServerIntegrationClient(String payloadRootPath, String baseURL,
104104
}
105105

106106
@Override
107-
protected CloseableHttpResponse executeMethod(HttpHost host,
108-
HttpRequestBase httpMethod,
109-
boolean requireAuthentication) throws IOException {
110-
String path = httpMethod.getURI().toString();
111-
audit.request(httpMethod);
107+
protected CloseableHttpResponse executeMethod(HttpRequestBase request, boolean requireAuthentication) throws IOException {
108+
String requestURI = request.getURI().toString();
109+
audit.request(request);
112110

113-
String payloadPath = path.substring(path.indexOf("/rest/"))
111+
String payloadPath = requestURI.substring(requestURI.indexOf("/rest/"))
114112
.replace("/rest/api/", "")
115113
.replace("/rest/", "")
116114
.replace('/', '-').replaceAll("[=%&?]", "_");
117115
payloadPath = payloadRootPath + payloadPath + ".json";
118116

119-
return loadResponseFromResources(getClass(), path, payloadPath);
117+
return loadResponseFromResources(getClass(), requestURI, payloadPath);
120118
}
121119

122120
@Override
@@ -155,9 +153,7 @@ private BitbucketClouldIntegrationClient(String payloadRootPath, String owner, S
155153
}
156154

157155
@Override
158-
protected CloseableHttpResponse executeMethod(HttpHost host,
159-
HttpRequestBase httpMethod,
160-
boolean requireAuthentication) throws IOException {
156+
protected CloseableHttpResponse executeMethod(HttpRequestBase httpMethod, boolean requireAuthentication) throws IOException {
161157
String path = httpMethod.getURI().toString();
162158
audit.request(httpMethod);
163159

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2025, Nikolas Falco
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package com.cloudbees.jenkins.plugins.bitbucket.filesystem;
25+
26+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
27+
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory;
28+
import java.io.FileNotFoundException;
29+
import jenkins.scm.api.SCMFile.Type;
30+
import org.junit.jupiter.api.Test;
31+
import org.jvnet.hudson.test.Issue;
32+
import org.jvnet.hudson.test.JenkinsRule;
33+
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
34+
35+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
36+
import static org.mockito.Mockito.mock;
37+
38+
class BitbucketSCMFileTest {
39+
40+
@WithJenkins
41+
@Issue("JENKINS-75157")
42+
@Test
43+
void test(JenkinsRule r) {
44+
BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient("https://acme.bitbucket.com");
45+
46+
BitbucketSCMFile parent = new BitbucketSCMFile(mock(BitbucketSCMFileSystem.class), client, "ref", "hash");
47+
BitbucketSCMFile file = new BitbucketSCMFile(parent, "pipeline_config.groovy", Type.REGULAR_FILE, "046d9a3c1532acf4cf08fe93235c00e4d673c1d2");
48+
assertThatThrownBy(file::content).isInstanceOf(FileNotFoundException.class);
49+
}
50+
}

0 commit comments

Comments
 (0)