Adding Content-Type & Expires Headers to S3 Assets

AWS’ excellent Node SDK makes it easy to upload files to S3 from your JS application:

const
  filename = '/path/to/local.file',
  s3 = new AWS.S3({
    accessKeyId: config.aws.access,
    secretAccessKey: config.aws.secret,
    region: config.aws.region
  });

s3.upload({
    Bucket: config.aws.buckets.cdn,
    Key: s3name,
    Body: fs.createReadStream(filename)
  },
  function (err, data) {
    if (err) {
      throw new Error('S3 upload failed');
    } 
    // do something
});

When served via HTTP the response headers for your asset will look something like the following:

HTTP/1.1 200 OK
Server: AmazonS3
Content-Type: application/octet-stream
Connection: close
Last-Modified: Fri, 24 Mar 2017 20:07:44 GMT
x-amz-request-id: 5C5D94EED7CCB23A
Accept-Ranges: bytes
Date: Tue, 28 Mar 2017 03:49:48 GMT
x-amz-id-2: wsLQxHsmlv07jZd4dzV2LJmRXa1BMNfSSepkeYTrYG2XGALVWm96QdWMmuSClOvDwGybQpfeAow=
Content-Length: 128259
Etag: "afe335175aec1bfc26a6cd3a4dba6819"

Two important HTTP headers are missing for your asset: Content-Type, which tells your browser how to parse & interpret the response, and Expires, which tells your browser how long to cache the asset for.

Luckily – though not entirely clear from the documenation – these shortcomings can be easily overcome when uploading the file by providing the necessary key/value pairs when uploading:

const
  RMR = require('rmr-lib');
  
  s3.upload({
    Bucket: config.aws.buckets.cdn,
    Key: s3name,
    ContentType: RMR.mime.fromPath(filename),
    Expires: new Date(Date.now() + (10 * 1000 * 60 * 60 * 24)), // 10 days
    Body: fs.createReadStream(filename)
  }, function(err, data) { 
    
  });

(The mime-type for a file is being set via rmr-lib.)

Inspecting the response for a GET request uploaded with the ContentType & Expires key/value pairs verifies that the necessary headers are being sent along with the asset:

HTTP/1.1 200 OK
…
Content-Type: image/png
Expires: Fri, 07 Apr 2017 03:00:23 GMT
…