I have a node.js app that periodically pushes some data to Amazon S3. I'm using a Put request to push a buffer over to S3.
I know that the "content-md5" parameter of the S3 request needs to be the base64 encoded Md5 hash of the content that I'm pushing. What has me confused is that 90% of the time, my requests succeed. The other 10% of the time, without my hashing method changing at all, Amazon gives me back "badDigest" error:
{ [Error: API error with HTTP Code: 400]
headers:
{
'content-type': 'application/xml',
'transfer-encoding': 'chunked',
date: 'Fri, 06 Apr 2012 02:20:14 GMT',
connection: 'close',
server: 'AmazonS3' },
code: 400,
document:
{ Code: 'BadDigest',
Message: 'The Content-MD5 you specified did not match what we received.',
ExpectedDigest: 'fPRrmxapcSHmI2gljme1Fg==',
CalculatedDigest: 'w6PoDxh2ty478+Mw2UwTrA==',
RequestId: '1018E7A80A8B0B00',
HostId: 'W/SK/OovQHlsi593DJ154pkHdOrUk3oMWmIGNdOKj3WaHa8cBknhB+7H5IdZLUjt' } }
Has anyone else experienced this randomness from S3 before? Am I missing something obvious?
Thanks!
You likely forgot to specify 'utf8' as parameter for update.
var status = 'काक्नोम्यत्क्नोम्यत्चं शक्नोम्यत्तुमतुम् ।तुम् ।् । नोपहिनस्ति माम् ॥';
var contentMd5 = crypto
.createHash('md5')
.update(status, 'utf8')
.digest('base64');
Without it works in the most cases but not when your string includes multibyte characters.
The aws-sdk will automatically calculate the ContentMD5 and ContentLength values for you. If you have a UTF-8 string and you are using '।्'.length to set the ContentLength value S3 will return the BadDigest error. So the solution in my case was just let the aws-sdk calculate the ContentMD5 and ContentLength values.
Related
I am using an AWS Lambda implementation with Node.js to generate a PDF file. I have the following callback that returns the pdf in an encoded base64 result. This works great for me:
return callback(null, {
statusCode: 200,
body: new Buffer(data).toString('base64'),
isBase64Encoded: true,
headers: {
'Content-Type': 'text',
},
})
However, I would like to add further information with my response - not just the PDF bae64 encoded data, but some string type results that I can use further down my active application connected to this Lambda function. I'd like to return the base64 data and string data, something like this:
return callback(null, {
statusCode: 200,
body: JSON.stringify(
{
message: 'hello world',
report: new Buffer(data).toString('base64')
}
),
isBase64Encoded: true,
headers: {
'Content-Type': 'text',
},
})
But this is failing for me. How would I refactor the above to return both string data and base64 data? I'm also having to force the isBase64Encoded setting to true, which may clash with my new requirement to return both base64 and normal string data.
The Content-Type of your response is not text - since you are returning JSON, a application/json value would make more sense and might alleviate some of the issues you are having. It would be helpful if the post could be updated with some more relevant details of the errors you are encountering.
One other possible work-around would be to add the message (and any other string values) as HTTP headers on the base64 encoded response you already have working. Then your client can decode the HTTP response whose body contains the base64 encoded PDF and HTTP headers x-custom-message (or something similar) set to hello world.
I know this question is not new but all the solution I get for this are in PHP or my issue is different from them.
I am using MWS feed API to submit flat file for Price and Quantity Updates and always get the following error:
the Content-MD5 HTTP header you passed for your feed did not match the
Content-MD5 we calculated for your feed
I would like to ask 3 questions here:-
ContentMD5Value parameter is optional as given in doc, but if i not passed that than it will say that you must enter ContentMD5Value.
As in doc the ContentFeed which we are given to Amazon. Amazon create contentMD5 for that file and then compares that contentMD5 value with the contentMD5 value we send to Amazon.
If both match then OK, otherwise it will throw an error. But if suppose I will not send the file then also the same errors come that MD5 does not match. How is that possible? Which file are they calculating the MD5 for? Because I haven't send the file in ContentFeed.
If I send the contentMD5 in a header as well as parameter and sending the ContentFeed in body, I still get the error.
Note:- I am sending the contentMD5 in a header as well as in a parameters in form using request module and also calculating the signature with that and then pass the contentFeed in body.
I am using JavaScript (Meteor), I calculate the md5 using the crpyto module.
First, I think that my md5 is wrong but then I tried with an online website that will give me the md5 for a file the md5.
for my file is:
MD5 value: d90e9cfde58aeba7ea7385b6d77a1f1e
Base64Encodevalue: ZDkwZTljZmRlNThhZWJhN2VhNzM4NWI2ZDc3YTFmMWU=
The flat file I downloaded from for Price and Quantity Updates:-
https://sellercentral.amazon.in/gp/help/13461?ie=UTF8&Version=1&entries=0&
I calculated the signature also by giving ContentMD5Value while calculating the signature.
FeedType:'_POST_FLAT_FILE_PRICEANDQUANTITYONLY_UPDATE_DATA_'
As, I read documentation for that I passed the MD5-header in headers and also send as parameter.
Amazon doc says:
Previously, Amazon MWS accepted the MD5 hash as a Content-MD5 header
instead of a parameter. Passing it as a parameter ensures that the MD5
value is part of the method signature, which prevents anyone on the
network from tampering with the feed content.
Amazon MWS will still accept a Content-MD5 header whether or not a
ContentMD5Value parameter is included. If both a header and parameter
are used, and they do not match, you will receive an
InvalidParameterValue error.
I am using the request module for http requests.
I am passing all the required keys, seller id, etc. in form of request module and passing the FeedContent in body.
I tried sending the file as follows:
Method for submitFeed is:-
submitFeed : function(){
console.log("submitFeedAPI running..");
app = mwsReport({auth: {sellerId:'A4TUFSCXD64V3', accessKeyId:'AKIAJBU3FTBCJUIZWF', secretKey:'Eug7ZbaLljtrnGKGFT/DTH23HJ' }, marketplace: 'IN'});
app.submitFeedsAPI({FeedType:'_POST_FLAT_FILE_PRICEANDQUANTITYONLY_UPDATE_DATA_'},Meteor.bindEnvironment(function(err,response){
if(err){
console.log("error in submit feed...")
console.log(err)
}
else{
console.log("suuccess submit feed....")
console.log(response);
}
}))
Method that call Amazon submitFeedAPI is:-
var submitFeedsAPI = function(options, callback){
console.log("submitFeedsAPI running...");
var fileReadStream = fs.createReadStream('/home/parveen/Downloads/test/testting.txt');
var contentMD5Value = crypto.createHash('md5').update(file).digest('base64');
var reqForm = {query: {"Action": "SubmitFeed", "MarketplaceId": mpList[mpCur].id, "FeedType":options.FeedType,"PurgeAndReplace":false,"ContentMD5Value":contentMD5Value}};
mwsReqProcessor(reqForm, 'submitFeedsAPI', "submitFeedsAPIResponse", "submitFeedsAPIResult", "mwsprod-0000",false,file, callback);
}
also try
var fileReadStream = fs.createReadStream('/home/parveen/Downloads/test/testting.txt');
var base64Contents = fileReadStream.toString('base64');
var contentMD5Value = crypto.createHash('md5').update(base64Contents).digest('base64');
mwsReqProcessor function is as below:-
mwsReqProcessor = function mwsReqProcessor(reqForm, name, responseKey, resultKey, errorCode,reportFlag,file, callback) {
reqOpt = {
url: mwsReqUrl,
method: 'POST',
timeout: 40000,
body:{FeedContent: fs.readFileSync('/home/parveen/feedContentFile/Flat.File.PriceInventory.in.txt')},
json:true,
form: null,
headers: {
// 'Transfer-Encoding': 'chunked',
//'Content-Type': 'text/xml',
// 'Content-MD5':'ZDkwZTljZmRlNThhZWJhN2VhNzM4NWI2ZDc3YTFmMWU=',
// 'Content-Type': 'text/xml; charset=iso-8859-1'
'Content-Type':'text/tab-separated-values;charset=UTF-8'
},
}
reqOpt.form = mwsReqQryGen(reqForm);
var r = request(reqOpt, function (err, res, body){
console.log(err)
console.log(res)
})
// var form = r.form();
//form.append('FeedContent',fs.createReadStream('/home/parveen/feedContent//File/Flat.File.PriceInventory.in.txt'))
}
Method for mwsReqQryGen generation:-
mwsReqQryGen = function mwsReqQryGen(options) {
var method = (options && options.method) ? ('' + options.method) : 'POST',
host = (options && options.host) ? ('' + options.host) : mwsReqHost,
path = (options && options.path) ? ('' + options.path) : mwsReqPath,
query = (options && options.query) ? options.query : null,
returnData = {
"AWSAccessKeyId": authInfo.accessKeyId,
"SellerId": authInfo.sellerId,
"SignatureMethod": "HmacSHA256",
"SignatureVersion": "2",
"Timestamp": new Date().toISOString(),
"Version":"2009-01-01",
},
key;
if(query && typeof query === "object")
for(key in query)
if(query.hasOwnProperty(key)) returnData[key] = ('' + query[key]);
if(authInfo.secretKey && method && host && path) {
// Sort query parameters
var keys = [],
qry = {};
for(key in returnData)
if(returnData.hasOwnProperty(key)) keys.push(key);
keys = keys.sort();
for(key in keys)
if(keys.hasOwnProperty(key)) qry[keys[key]] = returnData[keys[key]];
var sign = [method, host, path, qs.stringify(qry)].join("\n");
console.log("..................................................")
returnData.Signature = mwsReqSignGen(sign);
}
//console.log(returnData); // for debug
return returnData;
};
I also tried with following:-
reqOpt = {
url: mwsReqUrl,
method: 'POST',
timeout: 40000,
json:true,
form: null,
body: {FeedContent: fs.createReadStream('/home/parveen/feedContentFile/Flat.File.PriceInventory.in.txt')},
headers: {
// 'Transfer-Encoding': 'chunked',
//'Content-Type': 'text/xml',
// 'Content-MD5':'ZDkwZTljZmRlNThhZWJhN2VhNzM4NWI2ZDc3YTFmMWU=',
// 'Content-Type': 'text/xml; charset=iso-8859-1'
},
}
I also tried without JSON and directly send the file read stream in the
body, i.e:
reqOpt = {
url: mwsReqUrl,
method: 'POST',
timeout: 40000,
form: null,
body: fs.createReadStream('/home/parveen/feedContentFile/Flat.File.PriceInventory.in.txt'),
headers: {
// 'Transfer-Encoding': 'chunked',
//'Content-Type': 'text/xml',
// 'Content-MD5':'ZDkwZTljZmRlNThhZWJhN2VhNzM4NWI2ZDc3YTFmMWU=',
// 'Content-Type': 'text/xml; charset=iso-8859-1'
},
}
But same error comes every time:
the Content-MD5 HTTP header you passed for your feed did not match the
Content-MD5 we calculated for your feed
I want to know where I am doing wrong or what is the right way to submit feed API and sending the file using request module.
I also tried with the code given on MWS to generate the MD5 but same
error occurred each time.
My .txt file as follows:
sku price quantity
TP-T2-00-M 2
Any help is much appreciated
finally i got the solution as Ravi said above. Actually there are few points i want to clear here for you all who are facing the same issue:-
Amazon marketplace API doc is not giving proper information and example. Even i guess the documentation is not updated . As in doc they said that ContentMD5Value parameter value is optional on this page
You can check there they clearly mention that the field is not required but if you not pass than they gives the error that you must pass content MD5 value.
So that is wrong. ContentMD5 is required attribute.
They said in the same doc that you need to send file data weather its a xml or flat-file in the field key name i.e. FeedContent.
But that is also not needed you can send the file with any name no
need to give FeedContent key for the file you just need to send the
file in stream.
They will give the same error of contentMD5 not match weather you send file or not because if they not found file than the contentMD5 you send will not match to that. SO if you are getting the ContentMD5 not match error than check the following:-
Check that you are generating the right MD5 code for your file you can check whether you are generating the right code or not by there java code they given on doc . You can get that from this link
Don't trust on online websites for generating the MD5 hash and base64 encoding.
If your MD5 is matched with the MD5 generated from Java code they given than one thing is clear that your MD5 is right so no need to change on that.
Once your MD5 is correct and after that also if you get the same error that is:-
Amazon MWS SubmitFeed Content-MD5 HTTP header did not match the
Content-MD5 calculated by Amazon
ContentMD5 not matched .Than you need to check only and only you file uploading mechanism.
Because now the file you are sending to Amazon is not either correct or you are not sending it in the right way.
Check for file upload
For checking whether or not you are sending the right file you need to check with following:-
You need to send the required parameters like sellerId, marketplaceId, AWSAccessKey etc. as query params.
You need to send the file in the form-data as multipart , if you are using the request module of node.js than you can see the above code given by Ravi.
you need to set the header as only:-
'Content-Type': 'application/x-www-form-urlencoded'
No need to send the header as chunked or tab separated etc because i don't need them any more they are even confuse me because somewhere someone write use this header on other place someone write use this header.
So finally as i am abel to submit this API i didn't need any of the header rather than application/x-www-form-urlencoded.
Example:-
reqOpt = {
url: mwsReqUrl,
method: 'POST',
formData: {
my_file: fs.createReadStream('file.txt')
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
qs: { }// all the parameters that you are using while creating signature.
Code for creating the contentMD5 is:-
var fileData= fs.readFileSync('/home/parveen/Downloads/test/feed.txt','utf8');
var contentMD5Value = crypto.createHash('md5').update(fileData).digest('base64');
As i am facing the issue that is because i am using form and form-data simultaneously via request module so i convert my form data with qs(query string) and file in form-data as multipart.
So in this way you can successfully submit the API for submit feed.
Amazon requires the md5 hash of the file in base64 encoding.
Your code:
var fileReadStream = fs.createReadStream('/path/to/file.txt');
var file = fileReadStream.toString('base64'); //'[object Object]'
var contentMD5Value = crypto.createHash('md5').update(file).digest('base64');
wrongly assumes that a readStream's toString() will produce the file contents, when, in fact, this method is inherited from Object and produces the string '[object Object]'.
Base64-encoding that string always produces the 'FEGnkJwIfbvnzlmIG534uQ==' that you mentioned.
If you want to properly read and encode the hash, you can do the following:
var fileContents = fs.readFileSync('/path/to/file.txt'); // produces a byte Buffer
var contentMD5Value = crypto.createHash('md5').update(fileContents).digest('base64'); // properly encoded
which provides results equivalent to the following PHP snippet:
$contentMD5Value = base64_encode(md5_file('/path/to/file.txt', true));
Hey sorry for late reply but why don't you try to send the file in multipart in the form-data request and other queryStrings in 'qs' property of request module.
You can submit the request as follows:-
reqOpt = {
url: mwsReqUrl,
method: 'POST',
formData: {
my_file: fs.createReadStream('file.txt')
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
qs: {
AWSAccessKeyId: '<your AWSAccessKeyId>',
SellerId: '<your SellerId>',
SignatureMethod: '<your SignatureMethod>',
SignatureVersion: '<your SignatureVersion>',
Timestamp: '<your Timestamp>',
Version: '<your Version>',
Action: 'SubmitFeed',
MarketplaceId: '<your MarketplaceId>',
FeedType: '_POST_FLAT_FILE_PRICEANDQUANTITYONLY_UPDATE_DATA_',
PurgeAndReplace: 'false',
ContentMD5Value: '<your file.txt ContentMD5Value>',
Signature: '<your Signature>'
}
}
request(reqOpt, function(err, res){
})
Probably, I'm too late, but here are key points for C#:
1) Multipart form-data didn't work at all. Finished with the following (simplified):
HttpContent content = new StringContent(xmlStr, Encoding.UTF8, "application/xml");
HttpClient client = new HttpClient();
client.PostAsync(query, content)
2) About query:
UriBuilder builder = new UriBuilder("https://mws.amazonservices.com/");
NameValueCollection query = HttpUtility.ParseQueryString(builder.Query);
query["AwsAccessKeyId"] = your_key_str;
query["FeedType"] = "_POST_ORDER_FULFILLMENT_DATA_";
... other required params
query["ContentMD5Value"] = Md5Base64(xmlStr);
builder.Query = query.ToString();
query = builder.ToString();
3) About Md5base64
public static string Md5Base64(string xmlStr)
{
byte[] plainTextBytes = Encoding.UTF8.GetBytes(xmlStr);
MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();
byte[] hash = provider.ComputeHash(plainTextBytes);
return Convert.ToBase64String(hash);
}
I'm successfully generating a signed url that I can then use for a limited time to download resources from my s3 bucket. However I'm trying to use the ResponseContentDisposition attribute in the params as documented here:
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getObject-property
I'm not sure if I'm doing this wrong but for some reason the headers are not being set. For example if I use the url I get back from s3.getSignedUrl:
curl -i "https://foo-dev.s3.amazonaws.com/images/foo.jpg?AWSAccessKeyId=AKIAICBHUC26S6B446PQ&Expires=1468359314&Signature=EeBqx1G83oeusarBl2KUbbCCBgA%3D&response-content-disposition=attachment%3B%20filename%3Ddata.jpg"
the headers are:
x-amz-id-2: SG9rjYQCcuqgKfjBmMbDQC2CNLcnqBAFzP7zINa99VYUwNijPOm5Ea/5fllZ6cnt/Qti7e26hbE=
x-amz-request-id: 2670068008525B1D
Date: Tue, 12 Jul 2016 21:26:16 GMT
Content-Disposition: inline; filename=foo.jpg
Last-Modified: Tue, 12 Jul 2016 00:47:23 GMT
ETag: "2a8e36651b24769170f4faa429f40f54"
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 43373
Server: AmazonS3
I'm setting this, using the javascript s3 sdk like this:
function tempRedirect(req, res) {
var filename = req.params[0];
var contentDisposition = 'attachment; filename=data.jpg';
var params = {
Bucket: S3_BUCKET,
ResponseContentDisposition: contentDisposition,
Key: checkTrailingSlash(getFileKeyDir(req)) + filename
};
var s3 = new aws.S3(s3Options);
s3.getSignedUrl('getObject', params, function(err, url) {
res.redirect(url);
});
};
The docs are pretty light and I can only find PHP examples but it does look like I'm setting content disposition correctly.
Anyone know what is going wrong here??
According to RFC- 2616, your value is malformed.
The expected format is attachment; filename="funny-cat.jpg". The filename is a quoted string.
And, my original assumption was that S3 was rejecting it as invalid and silently refusing to replace the value.
Subsequent tests reveal unexpected behavior: if Content-Disposition is not stored with the object, then &response-content-disposition=... works as expected, setting the response header. But if there is a header stored with the object, this query string parameter does not have the documented effect of "overriding" that value.
Conversely, &response-content-type=... does override a stored Content-Type: for the object.
That's what a few quick tests revealed for me.
But this appears to be a bug -- or more accurately, some kind of regression -- in S3. According to one support forum post, the behavior is actually inconsistent, sometimes working and sometimes not.
S3 is aware of this issue and we are working to resolve it. (2016-07-12)
https://forums.aws.amazon.com/thread.jspa?threadID=235006
I am using nodejs and the REST API to interact with bigquery. I am using the google-oauth-jwt module for JWT signing.
I granted a service account write permission. So far I can list projects, list datasets, create a table and delete a table. But when it comes to upload a file via multipart POST, I ran into two problems:
gzipped json file doesn't work, I get an error saying "end boundary missing"
when I use uncompressed json file, I get a 401 unauthorized error
I don't think this is related to my machine's time being out of sync since other REST api calls worked as expected.
var url = 'https://www.googleapis.com/upload/bigquery/v2/projects/' + projectId + '/jobs';
var request = googleOauthJWT.requestWithJWT();
var jobResource = {
jobReference: {
projectId: projectId,
jobId: jobId
},
configuration: {
load: {
sourceFormat: 'NEWLINE_DELIMITED_JSON',
destinationTable: {
projectId: projectId,
datasetId: datasetId,
tableId: tableId
},
createDisposition: '',
writeDisposition: ''
}
}
};
request(
{
url: url,
method: 'POST',
jwt: jwtParams,
headers: {
'Content-Type': 'multipart/related'
},
qs: {
uploadType: 'multipart'
},
multipart: [
{
'Content-Type':'application/json; charset=UTF-8',
body: JSON.stringify(jobResource)
},
{
'Content-Type':'application/octet-stream',
body: fileBuffer.toString()
}
]
},
function(err, response, body) {
console.log(JSON.parse(body).selfLink);
}
);
Can anyone shine some light on this?
P.S. the documentation on bigquery REST api is not up to date on many things, wish the google guys can keep it updated
Update 1:
Here is the full HTTP request:
POST /upload/bigquery/v2/projects/239525534299/jobs?uploadType=multipart HTTP/1.1
content-type: multipart/related; boundary=71e00bd1-1c17-4892-8784-2facc6998699
authorization: Bearer ya29.AHES6ZRYyfSUpQz7xt-xwEgUfelmCvwi0RL3ztHDwC4vnBI
host: www.googleapis.com
content-length: 876
Connection: keep-alive
--71e00bd1-1c17-4892-8784-2facc6998699
Content-Type: application/json
{"jobReference":{"projectId":"239525534299","jobId":"test-upload-2013-08-07_2300"},"configuration":{"load":{"sourceFormat":"NEWLINE_DELIMITED_JSON","destinationTable":{"projectId":"239525534299","datasetId":"performance","tableId":"test_table"},"createDisposition":"CREATE_NEVER","writeDisposition":"WRITE_APPEND"}}}
--71e00bd1-1c17-4892-8784-2facc6998699
Content-Type: application/octet-stream
{"practiceId":2,"fanCount":5,"mvp":"Hello"}
{"practiceId":3,"fanCount":33,"mvp":"Hello"}
{"practiceId":4,"fanCount":71,"mvp":"Hello"}
{"practiceId":5,"fanCount":93,"mvp":"Hello"}
{"practiceId":6,"fanCount":92,"mvp":"Hello"}
{"practiceId":7,"fanCount":74,"mvp":"Hello"}
{"practiceId":8,"fanCount":100,"mvp":"Hello"}
{"practiceId":9,"fanCount":27,"mvp":"Hello"}
--71e00bd1-1c17-4892-8784-2facc6998699--
You are most likely sending duplicate content-type headers to the Google API.
I don't have the capability to effortlessly make a request to Google BigQuery to test, but I'd start with removing the headers property of your options object to request().
Remove this:
headers: {
'Content-Type': 'multipart/related'
},
The Node.js request module automatically detects that you have passed in a multipart array, and it adds the appropriate content-type header. If you provide your own content-type header, you most likely end up with a "duplicate" one, which does not contain the multipart boundary.
If you modify your code slightly to print out the actual headers sent:
var req = request({...}, function(..) {...});
console.log(req.headers);
You should see something like this for your original code above (I'm using the Node REPL):
> req.headers
{ 'Content-Type': 'multipart/related',
'content-type': 'multipart/related; boundary=af5ed508-5655-48e4-b43c-ae5be91b5ae9',
'content-length': 271 }
And the following if you remove the explicit headers option:
> req.headers
{ 'content-type': 'multipart/related; boundary=49d2371f-1baf-4526-b140-0d4d3f80bb75',
'content-length': 271 }
Some servers don't deal well with multiple headers having the same name. Hopefully this solves the end boundary missing error from the API!
I figured this out myself. This is one of those silly mistakes that would have you stuck for the whole day and at the end when you found the solution you would really knock on your own head.
I got the 401 by typing the selfLink URL in the browser. Of course it's not authorized.
I have the following code, where message is a JSON String. I am trying to upload this to s3 with the md5 of message as the destination filename. I am getting a '505' statusCode. I am new to NodeJS and not sure what I am doing wrong here?
knoxInitParams =
'key': awsKey
'secret': awsPrivateKey
'bucket': bucket
client = knox.createClient knoxInitParams
buff = new Buffer message
reqHeader =
'Content-Length': buff.length
'Content-Type': 'text/plain'
'x-amz-acl': 'private'
req = client.put '/tmp/xxx.txt', reqHeader
req.on 'response', (res) ->
console.log res.statusCode
console.log res.headers
if res.statusCode is 200
console.log res.url
req.on 'error', (err) ->
console.error "S3 Error: ", err
req.end buff
Edit:
Changed the destination to hardcode it, as a reply below pointed out that was causing the issue. However, I am now getting a 403 :(
Your code looks fine.
Make sure your date/time are correct. ntpdate -s pool.ntp.org.
Quick note, I ran into this issue, too, but my error was that I had a space in my filename.
var req = client.put('/tmp/x xx.txt', reqHeader);
I wrapped the filename, like this
var req = client.put(encodeURIComponent('/tmp/x xx.txt'))
Most likely your bug is here:
req = client.put destination.toLowerCase + '.txt', reqHeader
You probably want to invoke destination.toLowerCase:
req = client.put destination.toLowerCase() + '.txt', reqHeader
On the other hand, I think it's wholly unnecessary -- it will be lowercase already.
On a side note, you may want to look into unit testing -- it's a great way of catching these kinds of bugs! If I were you, I would add a function, say getFileName:
getFileName = (contents) ->
crypto.createHash('md5').update(contents).digest('hex') + '.txt'
Now you can easily test this function with nodeunit, mocha, jasmine or any of the other great test utilities, and make sure that it always returns what you expect -- and if not, help you notice immediately where the error is.
I can also heartily recommend node's debugger, which also helps you catch these bugs.
When I try your exact code with my own S3 account, it works fine:
$ coffee test
200
{ 'x-amz-id-2': 'f5C32nQHlE0WI8jtNFEZykRFAdrM8ZdBzgeAxc23bnJ2Ti4bYKmcY3pX/KpEzyZg',
'x-amz-request-id': 'B41AACFF85661C2E',
date: 'Tue, 01 May 2012 23:15:39 GMT',
etag: '"44b25eb6d36a88713b7260d8db15b24b"',
'content-length': '0',
server: 'AmazonS3' }
Check your ID/key/bucket, and date/time as #skrewler suggests.