Jenkins unexpected exception java.nio.channels.ClosedByInterruptException when deleting a big folder from Artifactory - jenkins

In my Jenkins shared lib, I’ve created a class called ArtifactManager which performs docker cleanup from Artifactory when branch is deleted.
When there is a really massive directory of docker images to delete (~50 GB), I’m getting an unexpected interrupt:
java.nio.channels.ClosedByInterruptException
at java.base/java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:199)
at java.base/sun.nio.ch.FileChannelImpl.endBlocking(FileChannelImpl.java:162)
at java.base/sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:285)
at org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverWriter.<init>(RiverWriter.java:109)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.saveProgram(CpsThreadGroup.java:560)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.saveProgram(CpsThreadGroup.java:537)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.saveProgramIfPossible(CpsThreadGroup.java:520)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:444)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.access$400(CpsThreadGroup.java:97)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:315)
at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:279)
at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:67)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:139)
at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28)
at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:68)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
Finished: FAILURE
My code is not included in the stack trace…
The code which performs the HTTP requests is:
def deleteArtifact(String pathOnServer){
def responseCode, data
Boolean deleted = true
Logger.printInfo(steps, "Deleting $server/$pathOnServer artifact from server")
steps.withCredentials([steps.usernamePassword(credentialsId: 'artifactory_user', passwordVariable: 'PASSWORD', usernameVariable: 'USERNAME')]){
(responseCode, data) = sendApiRequest("$server/$pathOnServer", "DELETE")
}
//if !2xxSuccessful() -> Http response codes family - 1xx: Informational, 2xx: Success, 3xx: Redirection, 4xx: Client Error, 5xx: Server Error
if((responseCode / 100 as int) != 2){
deleted = false
Logger.printError(steps, "Failed to delete: `$pathOnServer` from `$server`. Status Code: $responseCode")
Logger.printError(steps, "Data: $data")
}
return deleted
}
def sendApiRequest(String query, String httpMethod, String contentType ="", String data =""){
def responseCode
def responseData
def conn = new URL("${this.protocol}://$query").openConnection()
//Trying to increase timeout
conn.setConnectTimeout(15 * 60 *60 * 1000);
conn.setReadTimeout(15 * 60 *60 * 1000);
def auth = "${steps.env.USERNAME}:${steps.env.PASSWORD}".getBytes().encodeBase64().toString()
conn.setRequestProperty("Authorization", "Basic ${auth}")
conn.setRequestMethod(httpMethod)
if(contentType) conn.setRequestProperty( "Content-Type", contentType);
if(data){
conn.setDoOutput(true)
conn.getOutputStream().write(data.getBytes("UTF-8"));
//Note: the POST will start when you try to read a value from the HttpURLConnection, such as responseCode, inputStream.text, or getHeaderField('...'). (https://stackoverflow.com/a/47489805/10025322)
}
responseCode = conn.getResponseCode()
try{
responseData = conn.getInputStream().getText()
}catch(IOException e){
responseData = e.getMessage()
}
conn = null
return [responseCode, responseData]
}
I also tried to use a different library for performing the request but still getting the exception when Artifactory responses slowly (Delete huge directory):
def sendApiRequest(String query, String httpMethod, String contentType ="", String data =""){
def responseCode
def responseData
def http = new HTTPBuilder("${this.protocol}://$query")
http.request(Method.valueOf(httpMethod)) {
headers.'Authorization' = "Basic ${steps.env.USERNAME}:${steps.env.PASSWORD}".getBytes().encodeBase64().toString()
if (contentType) {
headers.'Content-Type' = contentType
requestContentType = contentType
}
if (data) {
body = data
}
response.success = { resp, reader ->
responseCode = resp.statusLine.statusCode
responseData = reader.text
}
response.failure = { resp, reader ->
responseCode = resp.statusLine.statusCode
responseData = reader.text
}
}
echo("RESPONSE_CODE: " + responseCode.toString() + " RESPONSE_DATA: " + responseData.toString())
return [responseCode, responseData]
}
I found that the ClosedByInterruptException may occur due to the following reasons:
The thread running the code was interrupted.
The connection to the remote server was closed due to a network error.
The server explicitly closed the connection.
The read/write operation timed out.
There was an interruption or failure in the underlying I/O operations.
The JVM was shut down while the operation was in progress.
Any idea how to handle/workaround?

The provided code doesn't work due to a bug in Jenkins' core (this issue was marked as fixed in jenkins >= 2.332.1LTS or 2.335)
As a workaround you can use:
def sendApiRequest(String query, String httpMethod, String contentType ="", String data =""){
/*
Caller must wrap function call with 'withCredentials' statement
with 'USERNAME' and 'PASSWORD' environment varaiables
*/
String apiCmd = """curl -s -w '###%{http_code}' -X '${httpMethod}' \
'${this.protocol}://$query' \
${contentType ? "-H 'Content-Type: $contentType'" : ''} \
-u "\$USERNAME:\$PASSWORD" \
${data ? "-d '${data}'" : ''}"""
def response = steps.sh(returnStdout: true, script: apiCmd, label: "Send API request to Artifactory").trim()
def responseCode = response.split("###")[1]
def responseData = response.split("###")[0]
try{
responseCode = responseCode.toInteger()
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid status code: `$responseCode` cannot cast to integer")
}
return [responseCode, responseData]
}

Related

Uploading txt file via POST request with HttpBuilder

I want to upload a txt file to a website using a POST request with HTTPBuilder and multipart/form-data
I've tried running my function and I get a HTTP 200 OK response, but the file doesn't appear on the website anywhere.
private Map fileUpload(String url, File file){
log.debug "doPost: $url body: ${file.getName()}"
FileBody fileBody = new FileBody(file,ContentType.APPLICATION_OCTET_STREAM)
def result = [:]
try {
def authSite = new HTTPBuilder(url)
authSite.auth.basic(user, password)
authSite.request(POST) { req ->
headers.Accept = "application/json, text/javascript, */*; q=0.01"
req.params.setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000)
req.params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 60000)
def mpe = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE)
mpe.addPart("gxt",fileBody)
req.setEntity(mpe)
response.success = { resp, reader ->
result = reader
}
response.failure = { resp, reader ->
println "My response handler got response: ${resp.statusLine}"
}
}
}
catch (e) {
log.debug("Could not perform POST request on URL $url", e)
throw e
}
return result
}
From debugging this is the status recieved
3695 [main] DEBUG org.apache.http.wire - << "HTTP/1.1 200 OK[\r][\n]"
3695 [main] DEBUG org.apache.http.wire - << "Date: Thu, 10 Jan 2019 07:34:06 GMT[\r][\n]"
Anything I'm doing wrong? I don't get any errors but it just seems like nothing happens.
I don't have anything conclusive, but I suspect there is something invalid with the way you set up the multipart upload.
To help figure this out, below is a standalone, working, multipart upload groovy script using HttpBuilder:
#Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.7.1')
#Grab('org.apache.httpcomponents:httpmime:4.2.1')
import org.apache.http.entity.mime.content.*
import org.apache.http.entity.mime.*
import groovyx.net.http.HTTPBuilder
import static groovyx.net.http.Method.POST
fileUpload('https://httpbin.org/post', new File('data.txt'))
Map fileUpload(String url, File file){
println "doPost: $url body: ${file.name}"
def result
try {
new HTTPBuilder(url).request(POST) { req ->
requestContentType = "multipart/form-data"
def content = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE)
content.addPart(file.name, new InputStreamBody(file.newInputStream(), file.name))
req.entity = content
// json might be something else (like a reader)
// depending on the response content type
response.success = { resp, json ->
result = json
println "RESP: ${resp.statusLine}, RESULT: $json"
}
response.failure = { resp, json ->
println "My response handler got response: ${resp.statusLine}"
}
}
} catch (e) {
println "Could not perform POST request on URL $url"
throw e
}
result
}
The script assumes a file data.txt with the data to post in the current directory. The script posts to httpbin.org as a working test endpoint, adjust accordingly to post to your endpoint instead.
Saving the above in test.groovy and executing will yield something like:
~> groovy test.groovy
doPost: https://httpbin.org/post body: data.txt
RESP: HTTP/1.1 200 OK, RESULT: [args:[:], data:, files:[data.txt:{ "foo": "bar" }], form:[:], headers:[Accept:*/*, Connection:close, Content-Type:multipart/form-data; boundary=ZVZuV5HAdPOt2Sv7ZjxuUHjd8sDAzCz9VkTqpJYP, Host:httpbin.org, Transfer-Encoding:chunked], json:null, origin:80.252.172.140, url:https://httpbin.org/post]
(note that first run will take a while as groovy grapes need to download the http-builder dependency tree)
perhaps starting with this working example and working your way back to your code would help you pinpoint whatever is not working in your code.

java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl in Jenkins pipeline

There are plenty of discussion about what it means and what to do about it, however the main solution of using #NonCPS doesn't seem to work. Here's the relevant piece of the code:
#NonCPS
def restCall(String method, String resource, String data = '') {
def URL url = new URL("${Params.REST_BASE_URI}/${resource}")
def HttpURLConnection connection = url.openConnection()
withCredentials([usernamePassword(credentialsId: 'restful-api', passwordVariable: 'RA_PASS', usernameVariable: 'RA_USER')]) {
String encoded = Base64.getEncoder().encodeToString(("${env.RA_USER}:${env.RA_PASS}").getBytes(StandardCharsets.UTF_8))
connection.setRequestProperty("Authorization", "Basic ${encoded}");
}
connection.setRequestProperty("content-type", "application/json");
connection.setRequestMethod(method)
connection.doOutput = true
if (data != '') {
def writer = new OutputStreamWriter(connection.outputStream)
writer.write(data)
writer.flush()
writer.close()
}
connection.connect();
def statusCode = connection.responseCode
if (statusCode != 200 && statusCode != 201) {
throw new Exception(connection.getErrorSteam().text)
}
return connection.content.text
}
Note that it does have #NonCPS on the function. However executing this still produces the same error:
an exception which occurred:
in field groovy.lang.Reference.value
in object groovy.lang.Reference#1375b00
in field WorkflowScript$_bitbucketCall_closure1.connection
in object WorkflowScript$_bitbucketCall_closure1#b3001c
in field org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.closures
in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup#144b2a6
in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup#144b2a6
Caused: java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:860)
at ...
How can I solve it?
Turns out, the #NonCPS annotation isn't needed to achieve what I'm after. Instead, all I need to do is to ensure that the there are no non-serializable variables still initialised at the end of the method call. Therefore the following method works fine:
def restCall(String method, String resource, String data = '') {
def URL url = new URL("${Params.REST_BASE_URI}/${resource}")
def HttpURLConnection connection = url.openConnection()
withCredentials([usernamePassword(credentialsId: 'restful-api', passwordVariable: 'RA_PASS', usernameVariable: 'RA_USER')]) {
String encoded = Base64.getEncoder().encodeToString(("${env.RA_USER}:${env.RA_PASS}").getBytes(StandardCharsets.UTF_8))
connection.setRequestProperty("Authorization", "Basic ${encoded}");
}
connection.setRequestProperty("content-type", "application/json");
connection.setRequestMethod(method)
connection.doOutput = true
if (data != '') {
def writer = new OutputStreamWriter(connection.outputStream)
writer.write(data)
writer.flush()
writer.close()
}
connection.connect();
def statusCode = connection.responseCode
if (statusCode != 200 && statusCode != 201) {
String text = connection.getErrorStream().text
connection = null
throw new Exception(text)
}
String text = connection.content.text
connection = null
}
The trick is to explicitly set connection = null before the end of the execution of the method.

Jenkins Docker container - 403 no valid crumb was included in the request

I'm setting up my Jenkins server, and on simple requests in the web interface, like creating a folder, a pipeline, a job, etc., I periodically get the following error:
HTTP ERROR 403
Problem accessing /job/Mgmt/createItem. Reason:
No valid crumb was included in the request
The server is using the Jenkins/Jenkins container, orchestrated by Kubernetes on a cluster on AWS created with kops. It sits behind a class ELB.
Why might I be experiencing this? I thought the crumb was to combat certain CSRF requests, but all I'm doing is using the Jenkins web interface.
Enabling proxy compatibility may help to solve this issue.
Go to Settings -> Security -> Enable proxy compatibility in CSRF Protection section
Some HTTP proxies filter out information that the default crumb issuer uses to calculate the nonce value. If an HTTP proxy sits between your browser client and your Jenkins server and you receive a 403 response when submitting a form to Jenkins, checking this option may help. Using this option makes the nonce value easier to forge.
After a couple of hours of struggling, I was able to make it work with curl:
export JENKINS_URL=http://localhost
export JENKINS_USER=user
export JENKINS_TOKEN=mytoken
export COOKIE_JAR=/tmp/cookies
JENKINS_CRUMB=$(curl --silent --cookie-jar $COOKIE_JAR $JENKINS_URL'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' -u $JENKINS_USER:$JENKINS_TOKEN)
echo $JENKINS_CRUMB
curl --cookie $COOKIE_JAR $JENKINS_URL/createItem?name=yourJob --data-binary #jenkins/config.xml -H $JENKINS_CRUMB -H "Content-Type:text/xml" -u $JENKINS_USER:$JENKINS_TOKEN -v
when calling the http://JENKINS_SERVER:JENKINS_PORT/JENKINS_PREFIX/crumbIssuer/api/json you receive a header ("Set-Cookie") to set a JSESSIONID, so you must supply it in the upcoming requests you issue,
the reason is that jenkins test for valid crumb in this manner: comparing the crumb you send in the request with a crumb it generates on the server side (using your session id),
you can see it in jenkins code: scroll down to method:
public boolean validateCrumb(ServletRequest request, String salt, String crumb)
it means you HAVE to include a session in the next requests (after fetching the crumb)!
so the curl --cookie must be used as ThiagoAlves stated in his solution
i use java so i used this next tester (HTTPClient would be prefered, but i wanted a simple java only example):
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Base64;
public class JobRunner
{
String jenkinsUser = "tester";
String jenkinsPassword = "1234"; // password or API token
String jenkinsServer = "localhost";
String jenkinsPort = "8080";
String jenkinsPrefix = "/jenkins";
String jSession = null;
String crumb = null;
HttpURLConnection connection = null;
String responseBody = "";
public void openConnection(String requestMethod, String relativeURL) throws Exception
{
// prepare the authentication string
String authenticationString = jenkinsUser + ":" + jenkinsPassword;
String encodedAuthenticationString = Base64.getEncoder().encodeToString(authenticationString.getBytes("utf-8"));
// construct the url and open a connection to it
URL url = new URL("http://" + jenkinsServer + ":" + jenkinsPort + jenkinsPrefix + relativeURL);
connection = (HttpURLConnection) url.openConnection();
// set the login info as a http header
connection.setRequestProperty("Authorization", "Basic " + encodedAuthenticationString);
// set the request method
connection.setRequestMethod(requestMethod);
}
public void readResponse() throws Exception
{
// get response body and set it in the body member
int responseCode = connection.getResponseCode();
switch (responseCode)
{
case 401:
System.out.println("server returned 401 response code - make sure your user/password are correct");
break;
case 404:
System.out.println("server returned 404 response code - make sure your url is correct");
break;
case 201:
case 200:
System.out.println("server returned " + responseCode + " response code");
InputStream responseBodyContent = connection.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(responseBodyContent));
String currentLine;
while ((currentLine = bufferedReader.readLine()) != null)
{
responseBody = responseBody + currentLine + "\n";
}
break;
default:
System.out.println("server returned error response code: " + responseCode);
break;
}
}
public void setSessionCookie() throws Exception
{
jSession = connection.getHeaderField("Set-Cookie");
System.out.println("jSession: " + jSession);
}
public void disconnect() throws Exception
{
if(connection!=null)
{
connection.disconnect();
connection = null;
responseBody = "";
}
}
public void getCrumb() throws Exception
{
try
{
openConnection("GET", "/crumbIssuer/api/json");
readResponse();
setSessionCookie();
int crumbIndex = responseBody.indexOf("crumb\":\"");
if(crumbIndex!=-1)
{
int crumbIndexEnd = responseBody.indexOf("\",\"", crumbIndex);
crumb = responseBody.substring(crumbIndex + "crumb\":\"".length(), crumbIndexEnd);
System.out.println(crumb);
}
}
finally
{
disconnect();
}
}
public void runJob() throws Exception
{
try
{
openConnection("POST", "/job/test/build");
connection.setDoOutput(true);
connection.setRequestProperty("Cookie", jSession);
connection.setRequestProperty("Jenkins-Crumb", crumb);
readResponse();
System.out.println("Post response: " + responseBody);
}
finally
{
disconnect();
}
}
public static void main(String[] args)
{
JobRunner jobRunner = new JobRunner();
try
{
jobRunner.getCrumb();
jobRunner.runJob();
}
catch (Exception err)
{
err.printStackTrace();
}
}
}

Groovy httpbuilder post list params

I'm trying to consume a web service from my grails project. I'm using httpbuilder 0.7.2. Below is my http client.
static def webServiceRequest(String baseUrl, String path, def data,method=Method.GET,contentType=ContentType.JSON){
def ret = null
def http = new HTTPBuilder(baseUrl)
http.request(method, contentType) {
uri.path = path
requestContentType = ContentType.URLENC
if(method==Method.GET)
uri.query = data
else
body = data
headers.'User-Agent' = 'Mozilla/5.0 Ubuntu/8.10 Firefox/3.0.4'
response.success = { resp, json ->
println "response status: ${resp.statusLine}"
ret = json
println '--------------------'
}
}
return ret
}
The issue is coming when i'm trying to send something like this:
def input = [:]
input['indexArray'] = [1,5]
api call
def response = webServiceRequest(url,uri,input,Method.POST)
when i'm printing the value of post data in my server it shows only last value of list.
{"indexArray":"5"}
it should show both 1 and 5
If you want to send json data using contenttype application/x-www-form-urlencoded you have to explicitly convert the data before adding it to the body, you can use (data as JSON).
I am using RESTClient (nice convenience wrapper on HTTPBuilder, https://github.com/jgritman/httpbuilder/wiki/RESTClient). It is as simple as this with Spock.
RESTClient restClient = new RESTClient("http://localhost:8080")
restClient.contentType = ContentType.JSON
Also it automatically parses the JSON data, so my Spock test is:
when: "we check the server health"
HttpResponseDecorator response = restClient.get([path : "/health"]) as HttpResponseDecorator
then: "it should be up"
response != null
200 == response.status
'application/json' == response.contentType

Cant connect to survey monkey API

I am trying to connect to the survey monekey API with this code, which is not working. It says "Invalid API key" even though I got the API from the API console.
public void fetch() {
String url = "https://api.surveymonkey.net/v2/surveys/get_survey_list?api_key=" + apiKey;
System.out.println("request being sent");
System.out.println(url);
JSONObject obj = new JSONObject();
try {
// byte[] postDataBytes = obj.toJSONString().getBytes("UTF-8");
URL ourl = new URL(url.toString());
HttpURLConnection conn = (HttpURLConnection) ourl.openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "bearer " + accessToken);
conn.setRequestProperty("Content-Type", "application/json");
conn.getRequestProperty(obj.toString().getBytes("UTF-8").toString());
int k = conn.getResponseCode();
System.out.println("The response code received is " + k);
if (conn.getResponseCode() != 200) {
throw new RuntimeException("Failed : HTTP error code : "
+ conn.getResponseCode());
}
BufferedReader br = new BufferedReader(new InputStreamReader(
(conn.getInputStream())));
String output;
System.out.println("Output from Server .... \n");
output = br.readLine();
System.out.println(output);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
Here's the error:
request being sent
https://api.surveymonkey.net/v2/surveys/get_survey_list?api_key=---API-KEY----
The response code received is 200
Output from Server ....
{"status":3,"errmsg":"Expected object or value"}
I just got this url from the API console.
Ensure you are using the API key associated with your developer account registered at http://developer.surveymonkey.com, not the sample API key the console uses to let you try requests. The sample api key is not meant to be used with apps, only on the API console.
That particular error is generated when an empty string is sent for the POST data. The API expects an empty object at minimum ("{}"). If the issue pointed out by Miles above was just a typo (using 'getRequestProperty' instead of 'setRequestProperty',) check if toString on an empty JSONObject is returning "" or "{}".

Resources