AWS S3 Rest API with Android Retrofit V2 library, uploaded image is damaged

11,515

Solution 1

RequestBody avatarBody = RequestBody.create(MediaType.parse("image"),file);
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), avatarBody);


@Multipart
@POST(url)
Call<ResponseBody> uploadImageAmazon(
            @Part MultipartBody.Part filePart);

I had same experience, and solved it by https://github.com/square/retrofit/issues/2424 this solution

Solution 2

I have used Retrofit 2 resolve and I use Body instead of Part for your RequestBody in interface

@PUT("")
Call<String> nameAPI(@Url String url, @Body RequestBody body);

and java code

// Prepare image file
File file = new File(pathImg);
RequestBody requestBody = RequestBody.create(MediaType.parse("image/jpeg"), file);

Call<String> call = SingletonApiServiceS3.getInstance().getService().nameAPI(
        path,
       requestBody
);
call.enqueue(new Callback<String>() {
    @Override
    public void onResponse(Call<String> call, final Response<String> response) {

        if (response.isSuccessful()) {
            // Your handling
        } else {
            // Your handling
       }
   }

   @Override
   public void onFailure(Call<String> call, Throwable t) {
       Toast.makeText(getContext(), "onFailure : "+t.getMessage().toString(),Toast.LENGTH_SHORT).show();
   }
});

Solution 3

I have the same problem, and as I use Fiddler checked the HTTP request content, I found retrofit 2.0.0 beta1 has a different with 1.9.0.

In my problem, the different of HTTP request content prevent server get the correct data.

In order to make a same HTTP request content, i do next steps using retrofit 2.0.0 deta1.


In the retrofit service, add a form-data header for the http request;

@Headers("Content-Type: multipart/form-data;boundary=95416089-b2fd-4eab-9a14-166bb9c5788b")

int retrofit 2.0.0 deta1, the header using @Multipart will get a data like this:

Content-Type: multipart/mixed

as the deafult value is mixed, and has no boundary title.


Do not using @Multipart to upload file, just using @Body RequestBody

if you using @Multipart to request Server, you have to pass param(file) through

@Part(key), then a new problem you will get. May be retrofit 2.0.0beta1 has a BUG ..., @Multipart generate a bad http request compile with 1.9.0.


When you call the method, you need pass MultipartRequestBody to @Body RequestBody

Using MultipartBuilder to create a MultipartRequestBody, when you new MultipartBuilder, call this consturt:

new MultipartBuilder("95416089-b2fd-4eab-9a14-166bb9c5788b")

the param is you set int @headers(boundary=)

builder.addFormDataPart(String name, String filename, RequestBody value)

This method will help form a data like below int HTTP request content:

Content-Disposition: form-data; name="imgFile"; filename="IMG_20150911_113029.jpg" Content-Type: image/jpg Content-Length: 1179469

RequestBody value is what you has generate in your code.

I just resolve this problem temporary.

Hope can help you!

Solution 4

You are sending a multipart payload, but forcing the Content-type to be image/jpeg. Your jpg is corrupt because S3 probably saved the multipart headers into your jpg file since you told it the whole message was a JPG. Since you do not actually have multiple parts to send, you can drop the Multipart annotation and use Body instead of Part for your RequestBody

public interface AwsS3 {

    @PUT("/{Key}")
    Call<String> upload(@Path("Key") String Key,
                @Header("Content-Length") long length,
                @Header("Accept") String accept,
                @Header("Host") String host,
                @Header("Date") String date,
                @Header("Content-type") String contentType,
                @Header("Authorization") String authorization,
                @Body RequestBody body);
}

You should also be able to remove explicitly setting the Content-type and Content-length headers.

Solution 5

I haven't used Retrofit 2, just Retrofit 1, so YMMV, but I believe that the typical way to do what you're trying to do is to use TypedFile where you are attempting to use RequestBody.

I'm guessing that Retrofit uses RequestBody internally.

You would create the TypedFile something like:

TypedFile typedFile = new TypedFile("multipart/form-data", new File("path/to/your/file"));

and your interface would be:

   @Multipart
    @PUT("/{Key}")
    Call<String> upload(@Path("Key") String Key,
                @Header("Content-Length") long length,
                @Header("Accept") String accept,
                @Header("Host") String host,
                @Header("Date") String date,
                @Header("Content-type") String contentType,
                @Header("Authorization") String authorization,
                @Part("Body") TypedFile body);
}

There's a decent example at https://futurestud.io/blog/retrofit-how-to-upload-files/

Share:
11,515
Anderson K
Author by

Anderson K

Updated on June 24, 2022

Comments

  • Anderson K
    Anderson K almost 2 years

    I'm trying upload a Image from my Android APP to Amazon AWS S3 and I need use AWS Restful API.

    I'm using Retrofit 2 to make to the request.

    My application is connecting successfully with Amazon S3 and performing the request as expected, but when I try to view the Image from the Bucket, the picture does not open. I downloaded the Image to my pc and tried to open but keep getting the message that the image is corrupted.

    Lets see my complete code bellow.

    My Gradle dependencies

    compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
    compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
    compile 'net.danlew:android.joda:2.8.2'
    

    Here is created a File and starts the request

    File file = new File(mCurrentPhotoPath);
    RequestBody body = RequestBody.create(MediaType.parse("image/jpeg"), file);
    uploadImage(body, "photo_name.jpeg");
    

    Retrofit Interface

    public interface AwsS3 {
    
        @Multipart
        @PUT("/{Key}")
        Call<String> upload(@Path("Key") String Key,
                    @Header("Content-Length") long length,
                    @Header("Accept") String accept,
                    @Header("Host") String host,
                    @Header("Date") String date,
                    @Header("Content-type") String contentType,
                    @Header("Authorization") String authorization,
                    @Part("Body") RequestBody body);
    }
    

    Utils class to the mount the credentials

    public class AWSOauth {
    
        public static String getOAuthAWS(Context context, String fileName)  throws Exception{
    
            String secret = context.getResources().getString(R.string.s3_secret);
            String access = context.getResources().getString(R.string.s3_access_key);
            String bucket = context.getResources().getString(R.string.s3_bucket);
    
            return gerateOAuthAWS(secret, access, bucket,fileName);
        }
    
        private static String gerateOAuthAWS(String secretKey, String accessKey, String bucket, String imageName) throws Exception {
    
            String contentType = "image/jpeg";
    
            DateTimeFormatter fmt = DateTimeFormat.forPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z").withLocale(Locale.US);
            String ZONE = "GMT";
            DateTime dt = new DateTime();
            DateTime dtLondon = dt.withZone(DateTimeZone.forID(ZONE)).plusHours(1);
            String formattedDate = dtLondon.toString(fmt);
    
            String resource = "/" + bucket + "/" + imageName;
    
            String stringToSign = "PUT" + "\n\n" + contentType + "\n" + formattedDate + "\n" + resource;
    
            Mac hmac = Mac.getInstance("HmacSHA1");
            hmac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1"));
    
            String signature = ( Base64.encodeToString(hmac.doFinal(stringToSign.getBytes("UTF-8")), Base64.DEFAULT)).replaceAll("\n", "");
    
            String oauthAWS = "AWS " + accessKey + ":" + signature;
    
            return  oauthAWS;
        }
    }
    

    Lastly the method to make a request

     public void uploadImage(RequestBody body, String fileName){
    
            String bucket = getString(R.string.s3_bucket);
    
            Retrofit restAdapter = new Retrofit.Builder()
                    .baseUrl("http://" + bucket + ".s3.amazonaws.com")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
    
            AwsS3 service = restAdapter.create(AwsS3.class);
    
            DateTimeFormatter fmt = DateTimeFormat.forPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z").withLocale(Locale.US);
            String ZONE = "GMT";
            DateTime dt = new DateTime();
            DateTime dtLondon = dt.withZone(DateTimeZone.forID(ZONE)).plusHours(1);
            String formattedDate = dtLondon.toString(fmt);
    
            try {
    
                String oauth = AWSOauth.getOAuthAWS(getApplicationContext(), fileName);
    
                Call<String> call = service.upload(fileName, body.contentLength(), "/**", bucket + ".s3.amazonaws.com", formattedDate,  body.contentType().toString(), oauth, body);
                call.enqueue(new Callback<String>() {
                    @Override
                    public void onResponse(Response<String> response) {
                        Log.d("tag", "response : " + response.body());
                    }
    
                    @Override
                    public void onFailure(Throwable t) {
                        Log.d("tag", "response : " + t.getMessage());
                    }
                });
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    I appreciate any helps, thanks in advance!

  • Anderson K
    Anderson K over 8 years
    Thanks for answer, I believe that be some wrong with my request body, because the request is performed ok, but I'm using Retrofit 2, TypedFile was removed from lib, and I cant make downgrade, I need use the version 2.
  • GreyBeardedGeek
    GreyBeardedGeek over 8 years
    Have you considered leaving off the explicit Content-Type and Content-Length headers, and letting Retrofit obtain them from the RequestBody ?
  • Anderson K
    Anderson K over 8 years
    This does not work AWS api rest requires that this header is sent.
  • Anderson K
    Anderson K over 8 years
    It make sense, I'll trying this approach later!
  • Anderson K
    Anderson K over 8 years
    So I tried, but does not work well. The image is not sent right!
  • Anderson K
    Anderson K over 8 years
    their approach seems ok, I figured out that the s3 AWS need that content is to be sent in the request body and not as multipart.
  • Phong Nguyen
    Phong Nguyen about 6 years
    I have a trouble about Content-Length of a part, Retrofit auto add Content-Length for the RequestBody and also each part, and server does not allowed this (I tested the API via Postman and it works well), github.com/square/okhttp/issues/2604 Do you have any ideas? or I have to throw out Retrofit?
  • Savitoj Cheema
    Savitoj Cheema about 5 years
    I needed this for s3 PUT call, Thanks a lot
  • Shivaraj Patil
    Shivaraj Patil over 4 years
    this helped thanks. I removed @Multipart and just added Body