Aws-sdk-js: PutObject on IOS 8

Created on 6 Oct 2014  路  10Comments  路  Source: aws/aws-sdk-js

I have problems with upload to S3 on IOS 8 Safari. This code works on desktop and android:

bucket.putObject(params, function (err, data) {
..
}

When I print variables to console, I get:
params:{\"Key\":\"somepath/.../somefilename\",\"Body\":\"...some text...\"}
(i removed data from Key and Body myself for this post, both are non-empty and look ok to me)

putObject returns error:
err:{\"message\":\"A header you provided implies functionality that is not implemented\",\"code\":\"NotImplemented\",\"time\":\"2014-10-06T11:16:17.545Z\",\"statusCode\":501,\"retryable\":true}"

I found https://github.com/aws/aws-sdk-ios/issues/40 and in comments there: https://forums.aws.amazon.com/thread.jspa?messageID=571835&tstart=0

Using https://sdk.amazonaws.com/js/aws-sdk-2.0.18.min.js

What can I do about this?

Thank you for your reply....

third-party

Most helpful comment

I was able to reproduce this issue in the simulator. It looks like iOS 8's Safari has a very aggressive caching behavior when it receives a Last-Modified response header, sending that as an If-Modified-Since to every subsequent request on the same URL (same S3 object) regardless of the HTTP method. This to me seems fishy, since it doesn't seem very sensible to do caching in this way.

I can reproduce with the following code:

var s3 = new AWS.S3({params: {Bucket: 'bucket', Key: 'key'});
s3.getObject(function() {
  s3.putObject({Body: 'someBody'}, function (err) {
    console.log(err);
  }
});

Fortunately I was also able to discover a workaround you could use until we find a good long-term solution for this issue.

Workaround: Use ResponseCacheControl or ResponseExpires whenever you call getObject:

If you always disable caching on your getObjects, S3 will never return a Last-Modified, meaning Safari will never cache this along with the URL. Reproduction code:

var s3 = new AWS.S3({params: {Bucket: 'bucket', Key: 'key'});
s3.getObject({ResponseCacheControl: 'no-cache', ResponseExpires: new Date(0)}, function() {
  s3.putObject({Body: 'someBody'}, function (err) {
    console.log(err);
  }
});

Either parameter works alone, you don't need to specify both.

We're looking into how to best resolve this going forward. I would prefer not to have to set a default value on ResponseCacheControl in order to workaround this issue, as that value gets added to your query string and doesn't look very nice (not to mention may not be the behavior you want on browsers that cache less aggressively).

All 10 comments

Are you able to debug the request in Safari and show which headers it is sending to S3? Based on the related issue, it looks like Safari is sending an If-Modified-Since header which S3 does not like.

If you can provide the x-amz-id-2 response header that would also be helpful.

I was able to reproduce this issue in the simulator. It looks like iOS 8's Safari has a very aggressive caching behavior when it receives a Last-Modified response header, sending that as an If-Modified-Since to every subsequent request on the same URL (same S3 object) regardless of the HTTP method. This to me seems fishy, since it doesn't seem very sensible to do caching in this way.

I can reproduce with the following code:

var s3 = new AWS.S3({params: {Bucket: 'bucket', Key: 'key'});
s3.getObject(function() {
  s3.putObject({Body: 'someBody'}, function (err) {
    console.log(err);
  }
});

Fortunately I was also able to discover a workaround you could use until we find a good long-term solution for this issue.

Workaround: Use ResponseCacheControl or ResponseExpires whenever you call getObject:

If you always disable caching on your getObjects, S3 will never return a Last-Modified, meaning Safari will never cache this along with the URL. Reproduction code:

var s3 = new AWS.S3({params: {Bucket: 'bucket', Key: 'key'});
s3.getObject({ResponseCacheControl: 'no-cache', ResponseExpires: new Date(0)}, function() {
  s3.putObject({Body: 'someBody'}, function (err) {
    console.log(err);
  }
});

Either parameter works alone, you don't need to specify both.

We're looking into how to best resolve this going forward. I would prefer not to have to set a default value on ResponseCacheControl in order to workaround this issue, as that value gets added to your query string and doesn't look very nice (not to mention may not be the behavior you want on browsers that cache less aggressively).

I can confirm ResponseCacheControl: 'no-cache' on getObject solves the problem.
Thank you for your help.

@mihapirnat I did some extra digging and found that this could be narrowed down to the following lines where we attach progress event listeners to the XHR object:

https://github.com/aws/aws-sdk-js/blob/master/lib/http/xhr.js#L38-L43

Attaching those listeners causes Safari to send PUT requests with If-Modified-Since headers, something I've never seen a browser do before. We could look into intelligently opting out of hooking into these events (at a feature loss), but I can't imagine any valid reason why Safari would ever have this behavior, so it seems to me like a Safari specific bug that should be reported.

We are speaking about Apple, so it must be a feature, not a bug.

I am satisfied with the workarounds for now, but I believe that in case of PutObject and similar methods that upload or change data, such headers (If-Modified-Since, If-None-Match, ...) _should simply be ignored by the amazon service_, since such behaviour is entirely optional anyway:

_10.3.5 304 Not Modified_
If the client has performed a conditional GET request and access is allowed, but the document has not been modified, the server _SHOULD_ respond with this status code.
(http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)

In my opinion this is the only possible good long-term solution, as there is usually little control over how a client's browser works.

@mihapirnat I would recommend opening a thread on the S3 forums to request support for the headers. That said, note that the RFC text you quoted applies to conditional GET requests, not PUT, which is what is happening here. Conditional PUTs don't really get much mention in the RFC, which is likely why it was left out of S3's implementation.

It's also important to note that if S3 were to ignore this header it would likely cause unwanted behavior for legitimate use cases, and would cause entirely different behavior if they started supporting it in the future (by honoring the conditional requests). In other words, your request today might go through, but if S3 started honoring Safari's request, your PUTs would silently be ignored at some point in the future-- and that would be worse than telling you up front that your request may not do what you want.

The problem here is that the semantics of Safari's request are flat out _wrong_. It is sending the following:

PUT /key
Host: bucket.s3.amazonaws.com
If-Modified-Since: SOME-OLD-DATE
...

Any web server that honored the conditional PUT would have to return a 302 and ignore the request. Safari is asking for the payload to be PUT only if the file _HAD_ been modified in S3 since the last GET, which is the opposite of what you want, and why I believe this is incorrect behavior for Safari.

@mihapirnat for more context, here's a summary of my findings with sample reproduction code (outside the SDK):

https://gist.github.com/lsegal/2ba84edf85422887a0c5

I pointed url at a bucket with a policy that only allows me to write a single key to reproduce the issue, though you could point it to any CORS-enabled resource and watch your wire logs to see what Safari is sending.

We've filed a Radar with Apple on this detailing the incorrect behaviors occurring in Safari, but at this point there is not much else we can do from the SDK side of things, so I am going to have to mark this as closed as a third-party issue. I would recommend filing a bug against Safari if you are still interested in pursuing this further and/or using one of the workarounds provided above. Thanks for bringing this to our attention!

I was having this problem when trying to delete an S3 object in a cordova app using the deleteObject method in the aws javascript sdk. Adding ResponseCacheControl: 'no-cache' when initially pulling the file down did the trick. Surprising that this issue still exists in iOS 3 years later.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread.

Was this page helpful?
0 / 5 - 0 ratings