Flutter/Dart upload pre-signed multipart zip to s3 adds extra header, which causes zipfile to be unzippable

Viewed 412

I'm trying to upload a zipfile to AWS S3 with a presigned url. I get the pre-signed url which seems to work great with Postman. With Flutter it also works, to some extend.

The problem however is that when uploading via Flutter (I used this snippet), it will upload the zipfile, but adds an extra 184 bytes to the header containing following:

--dart-http-boundary-z86XWhmTq935Bbfwfv.7UA+FIOU5nE4eqfUsZTh6PnGj+HBeAYH
content-type: application/octet-stream
content-disposition: form-data; name="out.zip"; filename="out.zip"

This header causes most built-in unzip tools to be unable to unzip the file.

I was able to unzip it using osx's built-in unzip command, but when downloading via Safari for example I get the 'Decompression Failed` message in the downloads section.

On Windows I failed to unzip altogether, even with 7-Zip.

Is there a way to remove the header so it uploads just the zip file as-is without adding extra headers?

Disclaimer: I'm not a Flutter/Dart dev by core.

Thanks!

EDIT 1: To create the signed URL (following AWS docs), I use following lambda function:

'use strict'

const AWS = require('aws-sdk');
AWS.config.update({ region: process.env.AWS_REGION });
const s3 = new AWS.S3();

const URL_EXPIRATION_SECONDS = 300;

exports.handler = async function(event, context, callback) {
  const body = event.body || {};
  return await getUploadURL(JSON.parse(event.body).projectNumber)
}

const getUploadURL = async function(projectNumber) {
  let Key = `${projectNumber}.zip`;
  
  if (!projectNumber) {
    const randomID = parseInt(Math.random() * 10000000)
    Key = `${randomID}.zip`;
  }
  
  // Get signed URL from S3
  const s3Params = {
    Bucket: process.env.UploadBucket,
    Key,
    Expires: URL_EXPIRATION_SECONDS,
    ContentType: 'application/zip',
    // This ACL makes the uploaded object publicly readable. You must also uncomment
    // the extra permission for the Lambda function in the SAM template.
    // ACL: 'public-read'
  }

  console.log('Params: ', s3Params)
  const uploadURL = await s3.getSignedUrlPromise('putObject', s3Params)

  return JSON.stringify({
    uploadURL: uploadURL,
    Key
  })
}
1 Answers

I'm suspicious of these lines in your snippet (remove them):

  request.headers.addAll({
    'Content-Type': mime,
  });

I don't think it's helpful to set the content type of the overall request - after all, that should remain as "multipart". If you want to give a content type to one of the parts, you do that where you add the file (i.e. on line 49 of your snippet). See the documentation of MultipartRequest.

var request = http.MultipartRequest('POST', uri)
  ..files.add(await http.MultipartFile.fromPath(
      'package', 'build/package.tar.gz',
      contentType: MediaType('application', 'x-tar'))); // content type added here
Related