Skip to content

Commit

Permalink
Support for multipart/form-data content-type in POST requests (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
kozell authored Jan 30, 2022
1 parent d93868e commit d98ff4e
Show file tree
Hide file tree
Showing 19 changed files with 381 additions and 24 deletions.
11 changes: 9 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ The following features are available in both Pipeline and traditional project ty
* You can specify a string that must be present in the response (if the string is not present, the
build fails)
* You can set a connection timeout limit (build fails if timeout is exceeded)
* You can set an "Accept" header
* You can set a "Content-Type" header
* You can set an "Accept" header directly
* You can set a "Content-Type" header directly
* You can set any custom header

=== Basic plugin features
Expand Down Expand Up @@ -102,6 +102,13 @@ You can also set custom headers:
def response = httpRequest customHeaders: [[name: 'foo', value: 'bar']]
----

You can send ``multipart/form-data`` forms:

[source,groovy]
----
def response = httpRequest httpMode: 'POST', formData: [[contentType: 'application/json', name: 'model', body: '{"foo": "bar"}'], [contentType: 'text/plain', name: 'file', fileName: 'readme.txt', uploadFile: 'data/lipsum.txt']]
----

For details on the Pipeline features, use the Pipeline snippet generator in the Pipeline job
configuration.

Expand Down
61 changes: 52 additions & 9 deletions src/main/java/jenkins/plugins/http_request/HttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import jenkins.plugins.http_request.auth.BasicDigestAuthentication;
import jenkins.plugins.http_request.auth.FormAuthentication;
import jenkins.plugins.http_request.util.HttpClientUtil;
import jenkins.plugins.http_request.util.HttpRequestFormDataPart;
import jenkins.plugins.http_request.util.HttpRequestNameValuePair;

/**
Expand Down Expand Up @@ -74,6 +75,7 @@ public class HttpRequest extends Builder {
private Boolean useSystemProperties = DescriptorImpl.useSystemProperties;
private boolean useNtlm = DescriptorImpl.useNtlm;
private List<HttpRequestNameValuePair> customHeaders = DescriptorImpl.customHeaders;
private List<HttpRequestFormDataPart> formData = DescriptorImpl.formData;

@DataBoundConstructor
public HttpRequest(@NonNull String url) {
Expand Down Expand Up @@ -239,6 +241,15 @@ public void setCustomHeaders(List<HttpRequestNameValuePair> customHeaders) {
this.customHeaders = customHeaders;
}

public List<HttpRequestFormDataPart> getFormData() {
return formData;
}

@DataBoundSetter
public void setFormData(List<HttpRequestFormDataPart> formData) {
this.formData = Collections.unmodifiableList(formData);
}

public String getUploadFile() {
return uploadFile;
}
Expand Down Expand Up @@ -277,6 +288,9 @@ protected Object readResolve() {
if (customHeaders == null) {
customHeaders = DescriptorImpl.customHeaders;
}
if (formData == null) {
formData = DescriptorImpl.formData;
}
if (validResponseCodes == null || validResponseCodes.trim().isEmpty()) {
validResponseCodes = DescriptorImpl.validResponseCodes;
}
Expand Down Expand Up @@ -371,27 +385,55 @@ FilePath resolveOutputFile(EnvVars envVars, AbstractBuild<?,?> build) {
return workspace.child(filePath);
}

FilePath resolveUploadFile(EnvVars envVars, AbstractBuild<?,?> build) {
if (uploadFile == null || uploadFile.trim().isEmpty()) {
FilePath resolveUploadFile(EnvVars envVars, AbstractBuild<?, ?> build) {
return resolveUploadFileInternal(uploadFile, envVars, build);
}

private static FilePath resolveUploadFileInternal(String path, EnvVars envVars, AbstractBuild<?, ?> build) {
if (path == null || path.trim().isEmpty()) {
return null;
}
String filePath = envVars.expand(uploadFile);
String filePath = envVars.expand(path);
try {
FilePath workspace = build.getWorkspace();
if (workspace == null) {
throw new IllegalStateException("Could not find workspace to check existence of upload file: " + uploadFile +
". You should use it inside a 'node' block");
throw new IllegalStateException(
"Could not find workspace to check existence of upload file: " + path
+ ". You should use it inside a 'node' block");
}
FilePath uploadFilePath = workspace.child(filePath);
if (!uploadFilePath.exists()) {
throw new IllegalStateException("Could not find upload file: " + uploadFile);
}
if (!uploadFilePath.exists()) {
throw new IllegalStateException("Could not find upload file: " + path);
}
return uploadFilePath;
} catch (IOException | InterruptedException e) {
throw new IllegalStateException(e);
}
}

List<HttpRequestFormDataPart> resolveFormDataParts(EnvVars envVars, AbstractBuild<?, ?> build) {
if (formData == null || formData.isEmpty()) {
return Collections.emptyList();
}

List<HttpRequestFormDataPart> resolved = new ArrayList<>(formData.size());

for (HttpRequestFormDataPart part : formData) {
String name = envVars.expand(part.getName());
String fileName = envVars.expand(part.getFileName());
FilePath resolvedUploadFile =
resolveUploadFileInternal(part.getUploadFile(), envVars, build);
String body = envVars.expand(part.getBody());

HttpRequestFormDataPart newPart = new HttpRequestFormDataPart(part.getUploadFile(),
name, fileName, part.getContentType(), body);
newPart.setResolvedUploadFile(resolvedUploadFile);
resolved.add(newPart);
}

return resolved;
}

@Override
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener)
throws InterruptedException, IOException
Expand Down Expand Up @@ -440,7 +482,8 @@ public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
public static final boolean wrapAsMultipart = true;
public static final Boolean useSystemProperties = false;
public static final boolean useNtlm = false;
public static final List <HttpRequestNameValuePair> customHeaders = Collections.emptyList();
public static final List<HttpRequestNameValuePair> customHeaders = Collections.emptyList();
public static final List<HttpRequestFormDataPart> formData = Collections.emptyList();

public DescriptorImpl() {
load();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import jenkins.plugins.http_request.auth.CredentialBasicAuthentication;
import jenkins.plugins.http_request.auth.CredentialNtlmAuthentication;
import jenkins.plugins.http_request.util.HttpClientUtil;
import jenkins.plugins.http_request.util.HttpRequestFormDataPart;
import jenkins.plugins.http_request.util.HttpRequestNameValuePair;
import jenkins.plugins.http_request.util.RequestAction;

Expand All @@ -87,6 +88,7 @@ public class HttpRequestExecution extends MasterToSlaveCallable<ResponseContentS

private final String body;
private final List<HttpRequestNameValuePair> headers;
private final List<HttpRequestFormDataPart> formData;

private final FilePath uploadFile;
private final String multipartName;
Expand Down Expand Up @@ -117,12 +119,15 @@ static HttpRequestExecution from(HttpRequest http,
FilePath uploadFile = http.resolveUploadFile(envVars, build);
Item project = build.getProject();

List<HttpRequestFormDataPart> formData = http.resolveFormDataParts(envVars, build);

return new HttpRequestExecution(
url, http.getHttpMode(), http.getIgnoreSslErrors(),
http.getHttpProxy(), http.getProxyAuthentication(),
body, headers, http.getTimeout(),
uploadFile, http.getMultipartName(), http.getWrapAsMultipart(),
http.getAuthentication(), http.isUseNtlm(), http.getUseSystemProperties(),
formData,

http.getValidResponseCodes(), http.getValidResponseContent(),
http.getConsoleLogResponseBody(), outputFile,
Expand All @@ -140,13 +145,17 @@ static HttpRequestExecution from(HttpRequestStep step, TaskListener taskListener
List<HttpRequestNameValuePair> headers = step.resolveHeaders();
FilePath outputFile = execution.resolveOutputFile();
FilePath uploadFile = execution.resolveUploadFile();
List<HttpRequestFormDataPart> formData = execution.resolveFormDataParts();

// TODO: resolveFormDataParts missing
Item project = execution.getProject();
return new HttpRequestExecution(
step.getUrl(), step.getHttpMode(), step.isIgnoreSslErrors(),
step.getHttpProxy(), step.getProxyAuthentication(),
step.getRequestBody(), headers, step.getTimeout(),
uploadFile, step.getMultipartName(), step.isWrapAsMultipart(),
step.getAuthentication(), step.isUseNtlm(), step.getUseSystemProperties(),
formData,

step.getValidResponseCodes(), step.getValidResponseContent(),
step.getConsoleLogResponseBody(), outputFile,
Expand All @@ -160,6 +169,7 @@ private HttpRequestExecution(
List<HttpRequestNameValuePair> headers, Integer timeout,
FilePath uploadFile, String multipartName, boolean wrapAsMultipart,
String authentication, boolean useNtlm, boolean useSystemProperties,
List<HttpRequestFormDataPart> formData,

String validResponseCodes, String validResponseContent,
Boolean consoleLogResponseBody, FilePath outputFile,
Expand Down Expand Up @@ -197,6 +207,7 @@ private HttpRequestExecution(

this.body = body;
this.headers = headers;
this.formData = formData;
this.timeout = timeout != null ? timeout : -1;
this.useNtlm = useNtlm;
if (authentication != null && !authentication.isEmpty()) {
Expand Down Expand Up @@ -293,9 +304,35 @@ private ResponseContentSupplier authAndRequest()
}

HttpClientUtil clientUtil = new HttpClientUtil();
// Create the simple body, this is the most frequent operation. It will be overridden
// later, if a more complex payload descriptor is set.
HttpRequestBase httpRequestBase = clientUtil.createRequestBase(new RequestAction(new URL(url), httpMode, body, null, headers));

if (uploadFile != null && (httpMode == HttpMode.POST || httpMode == HttpMode.PUT)) {
if (formData != null && !formData.isEmpty() && httpMode == HttpMode.POST) {
// multipart/form-data builder mode
MultipartEntityBuilder builder = MultipartEntityBuilder.create();

for (HttpRequestFormDataPart part : formData) {
if (part.getFileName() == null || part.getFileName().isEmpty()) {
ContentType textContentType = part.getContentType() == null || part.getContentType().isEmpty()
? ContentType.TEXT_PLAIN
: ContentType.create(part.getContentType());
builder.addTextBody(part.getName(), part.getBody(),
textContentType);
} else {
ContentType fileContentType = part.getContentType() == null || part.getContentType().isEmpty()
? ContentType.APPLICATION_OCTET_STREAM
: ContentType.create(part.getContentType());
builder.addBinaryBody(part.getName(),
new File(part.getResolvedUploadFile().getRemote()),
fileContentType, part.getFileName());
}
}

HttpEntity mimeBody = builder.build();
((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(mimeBody);
} else if (uploadFile != null && (httpMode == HttpMode.POST || httpMode == HttpMode.PUT)) {
// No form-data, but a singular uploadFile is set.
ContentType contentType = ContentType.APPLICATION_OCTET_STREAM;
for (HttpRequestNameValuePair header : headers) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(header.getName())) {
Expand All @@ -317,6 +354,7 @@ private ResponseContentSupplier authAndRequest()
entity = new FileEntity(new File(uploadFile.getRemote()), contentType);
}

// File upload overrides requestBody.
((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(entity);
httpRequestBase.setHeader(entity.getContentType());
httpRequestBase.setHeader(entity.getContentEncoding());
Expand Down
47 changes: 40 additions & 7 deletions src/main/java/jenkins/plugins/http_request/HttpRequestStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;

import jenkins.plugins.http_request.util.HttpRequestFormDataPart;
import jenkins.plugins.http_request.util.HttpRequestNameValuePair;

/**
Expand Down Expand Up @@ -58,6 +59,7 @@ public final class HttpRequestStep extends Step {
private Boolean useSystemProperties = DescriptorImpl.useSystemProperties;
private boolean useNtlm = DescriptorImpl.useNtlm;
private List<HttpRequestNameValuePair> customHeaders = DescriptorImpl.customHeaders;
private List<HttpRequestFormDataPart> formData = DescriptorImpl.formData;
private String outputFile = DescriptorImpl.outputFile;
private ResponseHandle responseHandle = DescriptorImpl.responseHandle;

Expand Down Expand Up @@ -206,6 +208,15 @@ public List<HttpRequestNameValuePair> getCustomHeaders() {
return customHeaders;
}

public List<HttpRequestFormDataPart> getFormData() {
return formData;
}

@DataBoundSetter
public void setFormData(List<HttpRequestFormDataPart> formData) {
this.formData = Collections.unmodifiableList(formData);
}

public String getOutputFile() {
return outputFile;
}
Expand Down Expand Up @@ -311,6 +322,7 @@ public static final class DescriptorImpl extends StepDescriptor {
public static final Boolean useSystemProperties = HttpRequest.DescriptorImpl.useSystemProperties;
public static final boolean useNtlm = HttpRequest.DescriptorImpl.useNtlm;
public static final List <HttpRequestNameValuePair> customHeaders = Collections.emptyList();
public static final List <HttpRequestFormDataPart> formData = Collections.emptyList();
public static final String outputFile = "";
public static final ResponseHandle responseHandle = ResponseHandle.STRING;

Expand Down Expand Up @@ -416,29 +428,50 @@ FilePath resolveOutputFile() {
}

FilePath resolveUploadFile() {
String uploadFile = step.getUploadFile();
if (uploadFile == null || uploadFile.trim().isEmpty()) {
return resolveUploadFileInternal(step.getUploadFile());
}

public Item getProject() throws IOException, InterruptedException {
return Objects.requireNonNull(getContext().get(Run.class)).getParent();
}

private FilePath resolveUploadFileInternal(String path) {
if (path == null || path.trim().isEmpty()) {
return null;
}

try {
FilePath workspace = getContext().get(FilePath.class);
if (workspace == null) {
throw new IllegalStateException("Could not find workspace to check existence of upload file: " + uploadFile +
throw new IllegalStateException("Could not find workspace to check existence of upload file: " + path +
". You should use it inside a 'node' block");
}
FilePath uploadFilePath = workspace.child(uploadFile);
FilePath uploadFilePath = workspace.child(path);
if (!uploadFilePath.exists()) {
throw new IllegalStateException("Could not find upload file: " + uploadFile);
throw new IllegalStateException("Could not find upload file: " + path);
}
return uploadFilePath;
} catch (IOException | InterruptedException e) {
throw new IllegalStateException(e);
}
}

public Item getProject() throws IOException, InterruptedException {
return Objects.requireNonNull(getContext().get(Run.class)).getParent();
List<HttpRequestFormDataPart> resolveFormDataParts() {
List<HttpRequestFormDataPart> formData = step.getFormData();
if (formData == null || formData.isEmpty()) {
return Collections.emptyList();
}

List<HttpRequestFormDataPart> resolved = new ArrayList<>(formData.size());

for (HttpRequestFormDataPart part : formData) {
HttpRequestFormDataPart newPart = new HttpRequestFormDataPart(part.getUploadFile(),
part.getName(), part.getFileName(), part.getContentType(), part.getBody());
newPart.setResolvedUploadFile(resolveUploadFileInternal(part.getUploadFile()));
resolved.add(newPart);
}

return resolved;
}
}
}
1 change: 1 addition & 0 deletions src/main/java/jenkins/plugins/http_request/MimeType.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum MimeType {
TEXT_HTML(ContentType.TEXT_HTML),
TEXT_PLAIN(ContentType.TEXT_PLAIN),
APPLICATION_FORM(ContentType.APPLICATION_FORM_URLENCODED),
APPLICATION_FORM_DATA(ContentType.MULTIPART_FORM_DATA),
APPLICATION_JSON(ContentType.create("application/json")),
APPLICATION_JSON_UTF8(ContentType.APPLICATION_JSON),
APPLICATION_TAR(ContentType.create("application/x-tar")),
Expand Down
Loading

0 comments on commit d98ff4e

Please sign in to comment.