How to upload images to s3 presigned URL in flutter?

Viewed 4147

I create a s3 presigned URL in typescript as below:

const params = {
      Bucket: myBucketName,
      Key: uuidv4(),
      Expires: 3600,
    };
s3.getSignedUrlPromise('putObject', params)

and I used below code in flutter to upload image to this url:

Option1:

var formData = FormData.fromMap({
      "file":
          await MultipartFile.fromFile(image.path, filename: "upload.png"),
    });
    var response = await Dio().put(url, data: formData);

Option2:

return Dio().put(
      url,
      data: image.openRead(),
      options: Options(
        contentType: "multiple/form-data",
        headers: {
          "Content-Length": image].lengthSync(),
        },
      ),
      onSendProgress: (int sentBytes, int totalBytes) {
        double progressPercent = sentBytes / totalBytes * 100;
        print("upload $progressPercent %");
      },
    )

in both cases I got 403 forbidden error response. How can I upload images to the presigned URL? Is there anything wrong in the generated side or flutter side?

I can upload the file with curl command like below:

 curl -XPOST -H "Content-Type: application/x-www-form-urlencoded"  --data-binary "@/Users/Pictures/IMG_4634.jpg" $PRESIGNED_URL

so the url does work.

5 Answers

I created this class to send an single image to s3 using pre-signed url, I'm using camera lib to send a photo to s3.

import 'dart:convert';
import 'dart:io';

import 'package:camera/camera.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';

class AwsApi {
  Future<String> uploadToSignedUrl({required XFile file, required String signedUrl}) async {
    Uri uri = Uri.parse(signedUrl);
    var response = await put(uri, body: await file.readAsBytes(), headers: {"Content-Type": "image/jpg"});

    return response;

  }
}

You can change await file.readAsBytes() to this file.readAsBytesSync(), if you are using File

I was getting 403 doing the same thing with presigned s3 urls and had to use a POST request which returned a 204 and successfully saved the file.

Using the http package I did the example like so:

import 'package:http/http.dart' as http;
var req = http.MultipartRequest('POST', Uri.parse(s3Url));
req.files.add(
  http.MultipartFile.fromBytes(
    'file',
    file.readAsBytesSync(),
    filename: filename,
  )
);
var res = await req.send();
print('UPLOAD: ${res.statusCode}');

This returns a StreamedResponse type so to get the body of the response you need to convert the response.

NOTE: I did have to add a lot of fields on the multipart request like AWSAccessKeyId and x-amz-security-token which I was returning from the s3 presigned URL response. I configured it very securely though so you may not need that but wanted to clarify.

If you can print the response from S3 somehow, that may reveal the error.

From the information available, I think the problem is that your are using a PUT request and not a POST request. Looking at the Dio documentation, it seems like you can use Dio().post. You may also need to set Content-Type like in your curl example.

If it still doesn't work, try adding -v to your curl command to see all of the headers it sends. It would be useful to get the network request that Dio sends, to compare against.

After some debugging, I found that this code can be used to fix it:

http.put(entry.value, body: image.readAsBytesSync())

I don't need to specify any header or any fields. And http.post is google enough to solve the issue.

I have tried all the solutions in this thread but it's not working

finally i found this blog: https://icircuit.net/flutter-upload-file-to-aws-s3/2885

this is the solution that works great with Dio:

Future<Response> sendFile(String url, File file) async {
  Dio dio = new Dio();
  var len = await file.length();
  var response = await dio.put(url,
      data: file.openRead(),
      options: Options(headers: {
        Headers.contentLengthHeader: len,
      } // set content-length
          ));
  return response;
}

Related