1

My goal is to upload a file to Nexus through Java. If you are unfamiliar with the Nexus REST API, I need to do a POST with a multipart/form-data content in order to send Nexus fields (like what directory my file belongs in) and my file contents.

The easy solution is to use the MultipartEntityBuilder from the Apache Components toolset, but I do not have access to the Apache HttpClient Mime library (though I do have access to all the core HTTP stuff) with no way to get access to it due to project restrictions. So I have to do something else. Again due to project restrictions, I only have access to the built-in Java libraries, and most of the Apache Components toolset, but I don't have any other HTTP libraries (like okhttp, Jersey, Restlet, and so on).

My code doesn't break, but I all I can get back from Nexus is HTTP error codes. I can get response headers back from Nexus just fine, I just can't get a configuration which is accepted by Nexus. I have tried doing POST manually in Firefox's network console tool thing, and it works and uploads a file there fine (so long as I have a valid cookie for authentication). So I know it's not the actual body that's being misformatted, since it works if I do it manually. The issue is something is being added or misconfigured in my HttpClient set up so Nexus can't understand what I mean.

What I have thus far is:

CloseableHttpClient clientConnection = ConnectionCommon.getCloseableHttpClient(ssl);
/*
 * getCloseableClient is a custom function which returns a client with
 * proper SSL configuration set up depending on what's needed.
 */
HttpPost post = new HttpPost(ConnectionCommon.constructURL(schema, host, portNum, dataType, repo));

String formBody = getFormBody();

/* Create the body entity.
 * I've tried various version of this, and none have worked.  I wanted to try
 * creating my own ContentType but I couldn't due to it being a final class.
 */
post.setEntity(new StringEntity(formBody));
// post.setEntity(new EntityBuilder.create().setText(formBody).build());

post.addHeader(new BasicHeader("Accept", "application/json"));
post.addHeader(new BasicHeader("Content-Type", "multipart/form-data; boundary=" + generatedBoundary));

// Get the server response, after generating our authentication context (if needed).
CloseableHttpResponse serverResponse = getCloseableResponse(clientConnection, post, auth);

// Process output and stuff here...

If I do:

post.setEntity(new StringEntity(formBody));
post.addHeader(new BasicHeader("Accept", "application/json"));
post.addHeader(new BasicHeader("Content-Type", "multipart/form-data; boundary=" + generatedBoundary));

or:

post.setEntity(EntityBuilder.create().setText(formBody).
               setContentType(ContentType.MULTIPART_FORM_DATA).build());
post.addHeader(new BasicHeader("Accept", "application/json"));
post.addHeader(new BasicHeader("Content-Type", "multipart/form-data; boundary=" + generatedBoundary));

I get a 422: Unprocessable Entity error, with no other information from Nexus as to what was misformatted. The response body is empty.

I thought perhaps setEntity was adding a Content-Type multipart/form-data for me, and because I was adding another in the header, it was confused. So I tried:

post.setEntity(new StringEntity(formBody);
post.addHeader(new BasicHeader("Accept", "application/json");

Which gives me 415: Unsupported Media Type. Again no response body. I sort of expected that one, as the Nexus server rejects anything you POST unless it's multipart/form-data.

So next I tried just letting the EntityBuilder make the header:

post.setEntity(EntityBuilder.create().setText(formBody).
               setContentType(ContentType.MULTIPART_FORM_DATA).build());
post.addHeader(new BasicHeader("Accept", "application/json"));

But then I get 500: Internal Server Error, with the response header saying:

java.io.IOException: Unable to get boundary for multipart.

Ok, well that's at least straightforward. Obviously I have to give it a boundary. But doing so then gives me the 422 as mentioned above.

Ok, well, let me just pass in the boundary as part of the MIME for the EntityBuilder:

post.setEntity(EntityBuilder.create().setText(formBody).
               setContentType(ContentType.create("multipart/form-data; boundary=" + generatedBoundary, Consts.UTF_8)).build());
post.addHeader(new BasicHeader("Accept", "application/json"));

But this didn't even run because, as it turns out, " ; " is an "invalid" character to pass into a ContentType.

java.lang.IllegalArgumentException: MIME type may not contain reserved characters.

Ok, so, I tried then adding the boundary by itself in the header, without the multipart process (which I didn't except to work since you have to specify them together):

post.setEntity(new StringEntity(formBody);
post.addHeader(new BasicHeader("Accept", "application/json");
post.addHeader(new BasicHeader("Content-Type", "multipart/form-data; boundary=" + generatedBoundary));

This, as expected, did not work. Surprisingly I got an actual html page back as my response body, but it just said 400: Bad Response.

Ok, so, "screw it" I said. I then went and tried to use Java's HTTP stuff and create a Multipart writer to do it by hand.

I found an answer here by user KDeogharkar that I basically copied verbatim to test. I'll put the code below for posterity, but I didn't come up with this solution myself.

public class MultipartUtility {
    private final String boundary;
    private static final String LINE_FEED = "\r\n";
    private HttpURLConnection httpConn;
    private String charset;
    private OutputStream outputStream;
    private PrintWriter writer;

    /**
     * This constructor initializes a new HTTP POST request with content type
     * is set to multipart/form-data
     *
     * @param requestURL
     * @param charset
     * @throws IOException
     */
    public MultipartUtility(String requestURL, String charset)
            throws IOException {
        this.charset = charset;

        // creates a unique boundary based on time stamp
        boundary = "===" + System.currentTimeMillis() + "===";
        URL url = new URL(requestURL);
        httpConn = (HttpURLConnection) url.openConnection();
        httpConn.setUseCaches(false);
        httpConn.setDoOutput(true);    // indicates POST method
        httpConn.setDoInput(true);
        httpConn.setRequestProperty("Content-Type",
                "multipart/form-data; boundary=" + boundary);
        outputStream = httpConn.getOutputStream();
        writer = new PrintWriter(new OutputStreamWriter(outputStream, charset),
                true);
    }

    /**
     * Adds a form field to the request
     *
     * @param name  field name
     * @param value field value
     */
    public void addFormField(String name, String value) {
        writer.append("--" + boundary).append(LINE_FEED);
        writer.append("Content-Disposition: form-data; name=\"" + name + "\"")
                .append(LINE_FEED);
        writer.append("Content-Type: text/plain; charset=" + charset).append(
                LINE_FEED);
        writer.append(LINE_FEED);
        writer.append(value).append(LINE_FEED);
        writer.flush();
    }

    /**
     * Adds a upload file section to the request
     *
     * @param fieldName  name attribute in <input type="file" name="..." />
     * @param uploadFile a File to be uploaded
     * @throws IOException
     */
    public void addFilePart(String fieldName, File uploadFile)
            throws IOException {
        String fileName = uploadFile.getName();
        writer.append("--" + boundary).append(LINE_FEED);
        writer.append(
                "Content-Disposition: form-data; name=\"" + fieldName
                        + "\"; filename=\"" + fileName + "\"")
                .append(LINE_FEED);
        writer.append(
                "Content-Type: "
                        + URLConnection.guessContentTypeFromName(fileName))
                .append(LINE_FEED);
        writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
        writer.append(LINE_FEED);
        writer.flush();

        FileInputStream inputStream = new FileInputStream(uploadFile);
        byte[] buffer = new byte[4096];
        int bytesRead = -1;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        outputStream.flush();
        inputStream.close();
        writer.append(LINE_FEED);
        writer.flush();
    }

    /**
     * Adds a header field to the request.
     *
     * @param name  - name of the header field
     * @param value - value of the header field
     */
    public void addHeaderField(String name, String value) {
        writer.append(name + ": " + value).append(LINE_FEED);
        writer.flush();
    }

    /**
     * Completes the request and receives response from the server.
     *
     * @return a list of Strings as response in case the server returned
     * status OK, otherwise an exception is thrown.
     * @throws IOException
     */
    public List<String> finish() throws IOException {
        List<String> response = new ArrayList<String>();
        writer.append(LINE_FEED).flush();
        writer.append("--" + boundary + "--").append(LINE_FEED);
        writer.close();

        // checks server's status code first
        int status = httpConn.getResponseCode();
        if (status == HttpURLConnection.HTTP_OK) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(
                    httpConn.getInputStream()));
            String line = null;
            while ((line = reader.readLine()) != null) {
                response.add(line);
            }
            reader.close();
            httpConn.disconnect();
        } else {
            throw new IOException("Server returned non-OK status: " + status);
        }
        return response;
    }
}

It seemed to write just fine, but I could never even get a response from the server this way, and my InputStreams were all null. I confirmed it wasn't uploading the file on the Nexus repository, so I have no idea why that wasn't working.

And just for clarity here is my form data (I've tried various boundaries as well, just to ensure the characters I am using aren't a problem):

--$$$010678954$$$
Content-Disposition: form-data; name="raw.directory"

PleaseWork
--$$$010678954$$$
Content-Disposition: form-data; name="raw.asset1"; filename="please.txt"
Content-Type: text/plain

Test test test
Foobar test
Random file contents blah
--$$$010678954$$$
Content-Disposition: form-data; name="raw.asset1.filename"

PleaseWorkPlease.txt
--$$$010678954$$$--

EDIT: I have gotten this to work through a Python script just fine, so I know it can't be a server issue. The only problem is I have to integrate this upload step as part of an existing Java program, so I can't just make a two-line Python script.

So now I beseech the Stack Overflow community to see if anyone can help.

Rezkin
  • 441
  • 1
  • 6
  • 17

0 Answers0