2
   public HttpResponseMessage run(
            @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {

There's only HttpRequestMessage parameter on the run() method in the Azure Functions(Java) spec. I need to declare and use MultipartHttpServletRequest to fetch a file from the multipart/data request. I'm trying but cannot see any way to cast HttpRequestMessag to MultipartHttpServletRequest.

Please give me some advice.

The HttpTrigger spec is : https://learn.microsoft.com/en-us/java/api/com.microsoft.azure.functions.annotation.httptrigger?view=azure-java-stable

----------------------- update -------------------------

The uploaded image is still corrupted. The size is exaclty same as the original one, but it seems like this :

enter image description here

I will paste the entire code. Please review it.

Function Class source :

public class HttpTriggerJava {
    private static final String storageConnectionString =
            "DefaultEndpointsProtocol=http;" +
                    "AccountName=00000;" +
                    "AccountKey=00000";

    @FunctionName("HttpTriggerJava")
    public HttpResponseMessage run(
            @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) throws Exception{

        context.getLogger().info("Java HTTP trigger processed a request.");

        CloudStorageAccount storageAccount = CloudStorageAccount.parse(storageConnectionString);
        CloudBlobClient blobClient = storageAccount.createCloudBlobClient();
        CloudBlobContainer container = blobClient.getContainerReference("contents");

        // here the "content-type" must be lower-case
        String contentType = request.getHeaders().get("content-type"); // Get content-type header

        String body = request.getBody().get(); // Get request body
        String boundary = contentType.split(";")[1].split("=")[1]; // Get boundary from content-type header
        int bufSize = 1024;
        InputStream in = new ByteArrayInputStream(body.getBytes()); // Convert body to an input stream
        MultipartStream multipartStream  = new MultipartStream(in, boundary.getBytes(), bufSize, null); // Using MultipartStream to parse body input stream
        boolean nextPart = multipartStream.skipPreamble();
        while(nextPart) {
            String header = multipartStream.readHeaders();
            System.out.println("");
            System.out.println("Headers:");
            System.out.println(header);
            System.out.println("Body:");
            if (header.contains("Content-Type: image/")) {
                int start = header.indexOf("filename=")+"filename=".length()+1;
                int end = header.indexOf("\r\n")-1;
                String filename = header.substring(start, end);
                System.out.println(filename);
                FileOutputStream fos = new FileOutputStream(filename);
                multipartStream.readBodyData(fos);

                File sourceFile = new File(filename);
                CloudBlockBlob blob = container.getBlockBlobReference(filename);
                blob.uploadFromFile(sourceFile.getAbsolutePath());

            } else {
                multipartStream.readBodyData(System.out);
            }
            System.out.println("");
            nextPart = multipartStream.readBoundary();
        }

        return request.createResponseBuilder(HttpStatus.OK).body("Success").build();
    }
}

And the HTML is :

<head>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
$(document).ready(function () {
    $("#myFile").change(function() {
      readURL(this);
    });

    $("#submit").click(function (event) {
        event.preventDefault();

        var form = $('#form')[0];
        var data = new FormData(form);

        $("#submit").prop("disabled", true);

        $.ajax({
            type: "POST",
            enctype: 'multipart/form-data',
            url: $(form).attr('action'),
            data: data,
            processData: false,
            contentType: false,
            cache: false,
            timeout: 600000,
            success: function (data) {
                $("#result").text(data);
                console.log("SUCCESS : ", data);
                $("#submit").prop("disabled", false);
            },
            error: function (e) {
                $("#result").text(e.responseText);
                console.log("ERROR : ", e);
                $("#submit").prop("disabled", false);
            }
        });
    });
});
function readURL(input) {
  if (input.files && input.files[0]) {
    var reader = new FileReader();

    reader.onload = function(e) {
      $('#blah').attr('src', e.target.result).show();
    }
    reader.readAsDataURL(input.files[0]);
  }
}
</script>
</head>

<body>
    <form id=form
        action="http://doopediafunctiontest.azurewebsites.net/api/HttpTriggerJava?code=00000"
        method="post" enctype="multipart/form-data">
        <p>
            <br /> <br /> <strong>My file:</strong><br /> <input type="file" id="myFile" name="myFile">
            <br /><img id="blah" src="#" alt="your image" style="display:none" />
        </p>
        <input id=submit type="submit" value="upload to Blob Storage">
    </form>

    <div id=result></div>
</body>

I compare the original image and the corrupted image by a hex editor. And I found some random hexes changed to 3f, it should be the reason. Maybe there's some encoding problem. But how can I fix this?

(Please click to enlarge) enter image description here

Deckard
  • 1,409
  • 6
  • 32
  • 59

2 Answers2

6

It sounds like you want to upload a file to your Azure Function with Http Trigger in Java via a HTML form with multipart/form-data like below.

<form method="POST" enctype="multipart/form-data" action="https://<your function app>/api/HttpTrigger-Java">
  File to upload: <input type="file" name="upfile"><br/>
  Notes about the file: <input type="text" name="note"><br/>
  <br/>
  <input type="submit" value="Press"> to upload the file!
</form>

However, there is not any class implements the interface HttpRequestMessage<T> and seems to not cast HttpRequestMessage to HttpServletRequest after I researched the source code of GitHub Repo Azure/azure-functions-java-library.

Per my experience, the only way is to parse the header and body of a multipart/form-data request to get the file. There is an answer of the similar SO thread Library and examples of parsing multipart/form-data from inputstream posted by the question owner, which includes the code using MultipartStream class of Apache Commons FileUpload that works after I test it.

Here is the Content-Type header and body of a multipart/form-data request received from Azure Function for Java.

Header Content-Type

content-type: multipart/form-data; boundary=----WebKitFormBoundaryT2TWuevX3RIYWRQF

multipart/form-data request body

------WebKitFormBoundaryT2TWuevX3RIYWRQF
Content-Disposition: form-data; name="upfile"; filename="z.txt"
Content-Type: text/plain
1234
ABCD
------WebKitFormBoundaryT2TWuevX3RIYWRQF
Content-Disposition: form-data; name="note"
test.txt
------WebKitFormBoundaryT2TWuevX3RIYWRQF--

Here is my sample code to fetch the file.

@FunctionName("HttpTrigger-Java")
public HttpResponseMessage run(
        @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
        final ExecutionContext context) {
    String contentType = request.getHeaders().get("content-type"); // Get content-type header
    // here the "content-type" must be lower-case
    String body = request.getBody().get(); // Get request body
    InputStream in = new ByteArrayInputStream(body.getBytes()); // Convert body to an input stream
    String boundary = contentType.split(";")[1].split("=")[1]; // Get boundary from content-type header
    int bufSize = 1024;
    MultipartStream multipartStream  = new MultipartStream(in, boundary.getBytes(), bufSize, null); // Using MultipartStream to parse body input stream
    // the code below comes from the SO thread above
    // you can fetch a file content from readBodyData 
    // after the headers Content-Disposition: form-data; name="upfile"; filename="test.txt" \n Content-Type: text/plain
    boolean nextPart = multipartStream.skipPreamble();
    while (nextPart) {
        String header = multipartStream.readHeaders();
        System.out.println("");
        System.out.println("Headers:");
        System.out.println(header);
        System.out.println("Body:");
        multipartStream.readBodyData(System.out);
        System.out.println("");
        nextPart = multipartStream.readBoundary();
    }
    return request.createResponseBuilder(HttpStatus.OK).body("Success").build();
}

The output of code above in terminal :

Headers:
Content-Disposition: form-data; name="upfile"; filename="test.txt"
Content-Type: text/plain


Body:
1234
ABCD


Headers:
Content-Disposition: form-data; name="note"


Body:
test.txt

Update: If upload an image, the output of the code above is like below.

Headers:
Content-Disposition: form-data; name="upfile"; filename="test.jpg"
Content-Type: image/png


Body:
<the binary content of an image>

So you can parse the header to get the filename value to use FileOutputStream to store it, as the code below.

while(nextPart) {
    String header = multipartStream.readHeaders();
    System.out.println("");
    System.out.println("Headers:");
    System.out.println(header);
    System.out.println("Body:");
    if (header.contains("Content-Type: image/")) {
        int start = header.indexOf("filename=")+"filename=".length()+1;
        int end = header.indexOf("\r\n")-1;
        String filename = header.substring(start, end);
        System.out.println(filename);
        FileOutputStream fos = new FileOutputStream(filename);
        multipartStream.readBodyData(fos);
    } else {
        multipartStream.readBodyData(System.out);
    }
    System.out.println("");
    nextPart = multipartStream.readBoundary();
}

Update 2:

I discovered there seems to be an issue of Azure Function for Java which may be a bug that will lose some bytes when uploading binary file, but it will not happend for uploading text file. So a workaround solution is to convert upload file to base64 string in browser to post to Azure Function and convert base64 content uploaded to the origin binary file in Azure Function.

Here is my testing HTML code.

File to upload: <input type="file" name="upfile" id="fileup"><br/>
<form method="POST" enctype="multipart/form-data" action="http://localhost:7071/api/HttpTrigger-Java">
  Notes about the file: <input type="text" name="note"><br/>
  <input type="hidden" name="file_base64" id="file_base64"><br/>
  <input type="submit" value="Press"> to upload the file!
</form>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">/script>
<script>
$(document).ready(function(){
    $("#fileup").change(function(){
        var v = $(this).val();
        var reader = new FileReader();
        reader.readAsDataURL(this.files[0]);
        reader.onload = function(e){
            console.log(e.target.result);
            $('#file_base64').val(e.target.result);
        };
    });
});
</script>

The form above will post the header and body of base64 file chunk as below.

Header:
Content-Disposition: form-data; name="file_base64"
Body:
data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAB.............

My Java code in Azure Function:

import java.io.ByteArrayOutputStream;
import java.util.Base64;

if (header.equals("Content-Disposition: form-data; name=\"file_base64\"")) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    multipartStream.readBodyData(baos);
    String content = baos.toString();
    // System.out.println(content);
    int index = content.indexOf(",")+1; // Get the index of base64 string in data-uploaded string
    byte[] imgBytes = Base64.getDecoder().decode(content.substring(index)); // convert image base64 string to image byte arrays
    ....
    // To upload image byte array to Blob Storage
    // You can get the upload image filename from the form input `note`, please notes the order of form input elements.
} else {
    multipartStream.readBodyData(System.out);
}
Peter Pan
  • 23,476
  • 4
  • 25
  • 43
  • Following your answer, I made a successful test for creating txt file. But for a JPEG file, the uploaded JPEG is corrupted. Is there any difference to create `txt` and `jpg`? – Deckard Feb 08 '19 at 01:10
  • @Deckard If you upload a image, you just need to output the binary content of `multipartStream` to other `OutputStream` like `FileOutputStream`. Please see my updated post. – Peter Pan Feb 08 '19 at 07:07
  • I added my entire source. Please review it. I don't know why it is corrupted. – Deckard Feb 08 '19 at 09:06
  • I compare the original image and the corrupted image by a hex editor. And I found some random hexes changed to 3f, it should be the reason. Maybe there's some encoding problem. But how can I fix this? – Deckard Feb 08 '19 at 09:28
  • @Deckard I re-test my code for uploading image, and I discovered there is an issue of Azure Funcs for Java which may be a bug that will lose some bytes when uploading binary file like image, but it will not happen for uploading text file. So I post a workaround solution to convert upload file to base64 string to fix the current issue. – Peter Pan Feb 12 '19 at 10:14
  • @Deckard If my new update post can fix up your issue, could you mark it answer? Thanks. Any concern, please feel free to let me know. – Peter Pan Feb 12 '19 at 10:33
  • It finally worked. I really appreciate your answer. And one more... If you work for MS, could you formally escalate this issue? So that they can solve it. It would be great if I would be noticed when it's done. – Deckard Feb 13 '19 at 04:12
  • @Deckard Only an issue report channel as I known is https://github.com/Azure/azure-functions-java-library/issues. Also, you can commit a feedback for the issue to [feedback.azure.com](https://feedback.azure.com/forums/34192--general-feedback). – Peter Pan Feb 18 '19 at 06:36
1

Why not use

HttpRequestMessage<Optional<byte[]>> request

instead of

HttpRequestMessage<Optional<String>> request

Transforming the body to String first messes things up.

CorBog
  • 11
  • 1