diff --git a/.changes/next-release/bugfix-S3-e7312ed3.json b/.changes/next-release/bugfix-S3-e7312ed3.json new file mode 100644 index 0000000000..5cdf96c1eb --- /dev/null +++ b/.changes/next-release/bugfix-S3-e7312ed3.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "S3", + "description": "add compatibility fallback S3#ExpiresString when S3#Expires field is not a date-time" +} \ No newline at end of file diff --git a/apis/s3-2006-03-01.min.json b/apis/s3-2006-03-01.min.json index a4d3ba447c..053e664534 100644 --- a/apis/s3-2006-03-01.min.json +++ b/apis/s3-2006-03-01.min.json @@ -2553,8 +2553,13 @@ "Expires": { "location": "header", "locationName": "Expires", + "deprecated": true, "type": "timestamp" }, + "ExpiresString": { + "location": "header", + "locationName": "ExpiresString" + }, "WebsiteRedirectLocation": { "location": "header", "locationName": "x-amz-website-redirect-location" @@ -2878,7 +2883,7 @@ "type": "structure", "members": { "LegalHold": { - "shape": "Saq" + "shape": "Sar" } }, "payload": "LegalHold" @@ -2912,7 +2917,7 @@ "type": "structure", "members": { "ObjectLockConfiguration": { - "shape": "Sat" + "shape": "Sau" } }, "payload": "ObjectLockConfiguration" @@ -2959,7 +2964,7 @@ "type": "structure", "members": { "Retention": { - "shape": "Sb1" + "shape": "Sb2" } }, "payload": "Retention" @@ -3094,7 +3099,7 @@ "type": "structure", "members": { "PublicAccessBlockConfiguration": { - "shape": "Sb8" + "shape": "Sb9" } }, "payload": "PublicAccessBlockConfiguration" @@ -3322,8 +3327,13 @@ "Expires": { "location": "header", "locationName": "Expires", + "deprecated": true, "type": "timestamp" }, + "ExpiresString": { + "location": "header", + "locationName": "ExpiresString" + }, "WebsiteRedirectLocation": { "location": "header", "locationName": "x-amz-website-redirect-location" @@ -3595,7 +3605,7 @@ "type": "structure", "members": { "Buckets": { - "shape": "Sbx" + "shape": "Sby" }, "Owner": { "shape": "S3q" @@ -3626,7 +3636,7 @@ "type": "structure", "members": { "Buckets": { - "shape": "Sbx" + "shape": "Sby" }, "ContinuationToken": {} } @@ -3725,7 +3735,7 @@ "shape": "S3q" }, "Initiator": { - "shape": "Scg" + "shape": "Sch" }, "ChecksumAlgorithm": {} } @@ -3733,7 +3743,7 @@ "flattened": true }, "CommonPrefixes": { - "shape": "Sch" + "shape": "Sci" }, "EncodingType": {}, "RequestCharged": { @@ -3798,7 +3808,7 @@ "locationName": "x-amz-request-payer" }, "OptionalObjectAttributes": { - "shape": "Scm", + "shape": "Scn", "location": "header", "locationName": "x-amz-optional-object-attributes" } @@ -3822,7 +3832,7 @@ "members": { "ETag": {}, "ChecksumAlgorithm": { - "shape": "Scs" + "shape": "Sct" }, "Size": { "type": "long" @@ -3840,7 +3850,7 @@ "shape": "S3q" }, "RestoreStatus": { - "shape": "Scv" + "shape": "Scw" } } }, @@ -3874,7 +3884,7 @@ "type": "integer" }, "CommonPrefixes": { - "shape": "Sch" + "shape": "Sci" }, "EncodingType": {}, "RequestCharged": { @@ -3936,7 +3946,7 @@ "locationName": "x-amz-expected-bucket-owner" }, "OptionalObjectAttributes": { - "shape": "Scm", + "shape": "Scn", "location": "header", "locationName": "x-amz-optional-object-attributes" } @@ -3951,7 +3961,7 @@ "Marker": {}, "NextMarker": {}, "Contents": { - "shape": "Sd4" + "shape": "Sd5" }, "Name": {}, "Prefix": {}, @@ -3960,7 +3970,7 @@ "type": "integer" }, "CommonPrefixes": { - "shape": "Sch" + "shape": "Sci" }, "EncodingType": {}, "RequestCharged": { @@ -4031,7 +4041,7 @@ "locationName": "x-amz-expected-bucket-owner" }, "OptionalObjectAttributes": { - "shape": "Scm", + "shape": "Scn", "location": "header", "locationName": "x-amz-optional-object-attributes" } @@ -4044,7 +4054,7 @@ "type": "boolean" }, "Contents": { - "shape": "Sd4" + "shape": "Sd5" }, "Name": {}, "Prefix": {}, @@ -4053,7 +4063,7 @@ "type": "integer" }, "CommonPrefixes": { - "shape": "Sch" + "shape": "Sci" }, "EncodingType": {}, "KeyCount": { @@ -4185,7 +4195,7 @@ "flattened": true }, "Initiator": { - "shape": "Scg" + "shape": "Sch" }, "Owner": { "shape": "S3q" @@ -4265,7 +4275,7 @@ "locationName": "x-amz-acl" }, "AccessControlPolicy": { - "shape": "Sdj", + "shape": "Sdk", "locationName": "AccessControlPolicy", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5109,7 +5119,7 @@ "locationName": "x-amz-sdk-checksum-algorithm" }, "Tagging": { - "shape": "Se9", + "shape": "Sea", "locationName": "Tagging", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5521,7 +5531,7 @@ "locationName": "x-amz-acl" }, "AccessControlPolicy": { - "shape": "Sdj", + "shape": "Sdk", "locationName": "AccessControlPolicy", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5622,7 +5632,7 @@ "locationName": "Key" }, "LegalHold": { - "shape": "Saq", + "shape": "Sar", "locationName": "LegalHold", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5684,7 +5694,7 @@ "locationName": "Bucket" }, "ObjectLockConfiguration": { - "shape": "Sat", + "shape": "Sau", "locationName": "ObjectLockConfiguration", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5751,7 +5761,7 @@ "locationName": "Key" }, "Retention": { - "shape": "Sb1", + "shape": "Sb2", "locationName": "Retention", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5836,7 +5846,7 @@ "locationName": "x-amz-sdk-checksum-algorithm" }, "Tagging": { - "shape": "Se9", + "shape": "Sea", "locationName": "Tagging", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5895,7 +5905,7 @@ "locationName": "x-amz-sdk-checksum-algorithm" }, "PublicAccessBlockConfiguration": { - "shape": "Sb8", + "shape": "Sb9", "locationName": "PublicAccessBlockConfiguration", "xmlNamespace": { "uri": "http://s3.amazonaws.com/doc/2006-03-01/" @@ -5976,12 +5986,12 @@ ], "members": { "InputSerialization": { - "shape": "Sez" + "shape": "Sf0" }, "ExpressionType": {}, "Expression": {}, "OutputSerialization": { - "shape": "Sfe" + "shape": "Sff" } } }, @@ -6015,7 +6025,7 @@ "shape": "S3t" }, "Tagging": { - "shape": "Se9" + "shape": "Sea" }, "UserMetadata": { "type": "list", @@ -6123,10 +6133,10 @@ } }, "InputSerialization": { - "shape": "Sez" + "shape": "Sf0" }, "OutputSerialization": { - "shape": "Sfe" + "shape": "Sff" }, "ScanRange": { "type": "structure", @@ -7731,13 +7741,13 @@ } } }, - "Saq": { + "Sar": { "type": "structure", "members": { "Status": {} } }, - "Sat": { + "Sau": { "type": "structure", "members": { "ObjectLockEnabled": {}, @@ -7760,7 +7770,7 @@ } } }, - "Sb1": { + "Sb2": { "type": "structure", "members": { "Mode": {}, @@ -7769,7 +7779,7 @@ } } }, - "Sb8": { + "Sb9": { "type": "structure", "members": { "BlockPublicAcls": { @@ -7790,7 +7800,7 @@ } } }, - "Sbx": { + "Sby": { "type": "list", "member": { "locationName": "Bucket", @@ -7803,14 +7813,14 @@ } } }, - "Scg": { + "Sch": { "type": "structure", "members": { "ID": {}, "DisplayName": {} } }, - "Sch": { + "Sci": { "type": "list", "member": { "type": "structure", @@ -7820,16 +7830,16 @@ }, "flattened": true }, - "Scm": { + "Scn": { "type": "list", "member": {} }, - "Scs": { + "Sct": { "type": "list", "member": {}, "flattened": true }, - "Scv": { + "Scw": { "type": "structure", "members": { "IsRestoreInProgress": { @@ -7840,7 +7850,7 @@ } } }, - "Sd4": { + "Sd5": { "type": "list", "member": { "type": "structure", @@ -7851,7 +7861,7 @@ }, "ETag": {}, "ChecksumAlgorithm": { - "shape": "Scs" + "shape": "Sct" }, "Size": { "type": "long" @@ -7861,13 +7871,13 @@ "shape": "S3q" }, "RestoreStatus": { - "shape": "Scv" + "shape": "Scw" } } }, "flattened": true }, - "Sdj": { + "Sdk": { "type": "structure", "members": { "Grants": { @@ -7879,7 +7889,7 @@ } } }, - "Se9": { + "Sea": { "type": "structure", "required": [ "TagSet" @@ -7890,7 +7900,7 @@ } } }, - "Sez": { + "Sf0": { "type": "structure", "members": { "CSV": { @@ -7920,7 +7930,7 @@ } } }, - "Sfe": { + "Sff": { "type": "structure", "members": { "CSV": { diff --git a/apis/s3-2006-03-01.normal.json b/apis/s3-2006-03-01.normal.json index 6d241b4b68..df2da5e12c 100644 --- a/apis/s3-2006-03-01.normal.json +++ b/apis/s3-2006-03-01.normal.json @@ -4582,6 +4582,9 @@ "Expires": { "type": "timestamp" }, + "ExpiresString": { + "type": "string" + }, "ExposeHeader": { "type": "string" }, @@ -5806,9 +5809,16 @@ }, "Expires": { "shape": "Expires", + "documentation": "Deprecated in favor of ExpiresString.", + "location": "header", + "locationName": "Expires", + "deprecated": true + }, + "ExpiresString": { + "shape": "ExpiresString", "documentation": "
The date and time at which the object is no longer cacheable.
", "location": "header", - "locationName": "Expires" + "locationName": "ExpiresString" }, "WebsiteRedirectLocation": { "shape": "WebsiteRedirectLocation", @@ -6495,9 +6505,16 @@ }, "Expires": { "shape": "Expires", + "documentation": "Deprecated in favor of ExpiresString.", + "location": "header", + "locationName": "Expires", + "deprecated": true + }, + "ExpiresString": { + "shape": "ExpiresString", "documentation": "The date and time at which the object is no longer cacheable.
", "location": "header", - "locationName": "Expires" + "locationName": "ExpiresString" }, "WebsiteRedirectLocation": { "shape": "WebsiteRedirectLocation", diff --git a/clients/s3.d.ts b/clients/s3.d.ts index cd6ead540f..16d5e9fe91 100644 --- a/clients/s3.d.ts +++ b/clients/s3.d.ts @@ -2263,6 +2263,7 @@ declare namespace S3 { export type ExpirationStatus = "Enabled"|"Disabled"|string; export type ExpiredObjectDeleteMarker = boolean; export type Expires = Date; + export type ExpiresString = string; export type ExposeHeader = string; export type ExposeHeaders = ExposeHeader[]; export type Expression = string; @@ -2895,9 +2896,13 @@ declare namespace S3 { */ ContentType?: ContentType; /** - * The date and time at which the object is no longer cacheable. + * Deprecated in favor of ExpiresString. */ Expires?: Expires; + /** + * The date and time at which the object is no longer cacheable. + */ + ExpiresString?: ExpiresString; /** * If the bucket is configured as a website, redirects requests for this object to another object in the same bucket or to an external URL. Amazon S3 stores the value of this header in the object metadata. This functionality is not supported for directory buckets. */ @@ -3282,9 +3287,13 @@ declare namespace S3 { */ ContentType?: ContentType; /** - * The date and time at which the object is no longer cacheable. + * Deprecated in favor of ExpiresString. */ Expires?: Expires; + /** + * The date and time at which the object is no longer cacheable. + */ + ExpiresString?: ExpiresString; /** * If the bucket is configured as a website, redirects requests for this object to another object in the same bucket or to an external URL. Amazon S3 stores the value of this header in the object metadata. This functionality is not supported for directory buckets. */ diff --git a/lib/services/s3.js b/lib/services/s3.js index 0c9b0e568c..0d0b11bbdc 100644 --- a/lib/services/s3.js +++ b/lib/services/s3.js @@ -119,6 +119,7 @@ AWS.util.update(AWS.S3.prototype, { * @api private */ setupRequestListeners: function setupRequestListeners(request) { + request.addListener('validateResponse', this.setExpiresString); var prependListener = true; request.addListener('validate', this.validateScheme); request.addListener('validate', this.validateBucketName, prependListener); @@ -1284,6 +1285,30 @@ AWS.util.update(AWS.S3.prototype, { var uploader = new AWS.S3.ManagedUpload(options); if (typeof callback === 'function') uploader.send(callback); return uploader; + }, + + /** + * @api private + */ + setExpiresString: function setExpiresString(response) { + // Check if response contains Expires value, and populate ExpiresString. + if (response && response.httpResponse && response.httpResponse.headers) { + if ('expires' in response.httpResponse.headers) { + response.httpResponse.headers.expiresstring = response.httpResponse.headers.expires; + } + } + + // Check if value in Expires is not a Date using parseTimestamp. + try { + if (response && response.httpResponse && response.httpResponse.headers) { + if ('expires' in response.httpResponse.headers) { + AWS.util.date.parseTimestamp(response.httpResponse.headers.expires); + } + } + } catch (e) { + console.log('AWS SDK', '(warning)', e); + delete response.httpResponse.headers.expires; + } } }); diff --git a/scripts/lib/set-s3-expires-string.js b/scripts/lib/set-s3-expires-string.js new file mode 100644 index 0000000000..6b2ad8b997 --- /dev/null +++ b/scripts/lib/set-s3-expires-string.js @@ -0,0 +1,53 @@ +/** + * Sets model overrides for S3 ExpiresString. + */ +module.exports = function setS3ExpiresString(model) { + if (model.metadata.serviceId === 'S3') { + var newShapes = {}; + for (var shapeId in model.shapes) { + newShapes[shapeId] = model.shapes[shapeId]; + if (shapeId === 'Expires') { + // preserve timestamp type for Expires. + newShapes[shapeId] = {}; + newShapes[shapeId].type = 'timestamp'; + + // add ExpiresString + newShapes['ExpiresString'] = { + type: 'string' + }; + } + } + model.shapes = newShapes; + + for (var operationKey in model.operations) { + var op = model.operations[operationKey]; + if (!op.output || !op.output.shape) { + continue; + } + var output = model.shapes[op.output.shape]; + if (!output || !output.members) { + continue; + } + if ('Expires' in output.members) { + var newMembers = {}; + for (var memberKey in output.members) { + newMembers[memberKey] = output.members[memberKey]; + if (memberKey === 'Expires') { + newMembers['ExpiresString'] = Object.assign({}, newMembers[memberKey], { + 'shape': 'ExpiresString', + 'location': 'header', + 'locationName': 'ExpiresString' + }); + + newMembers[memberKey].deprecated = true; + newMembers[memberKey].documentation = 'Deprecated in favor of ExpiresString.'; + } + } + output.members = newMembers; + } + } + + return true; + } + return false; +}; diff --git a/scripts/region-checker/allowlist.js b/scripts/region-checker/allowlist.js index c80de525c0..88f974e368 100644 --- a/scripts/region-checker/allowlist.js +++ b/scripts/region-checker/allowlist.js @@ -40,17 +40,17 @@ var allowlist = { '/services/s3.js': [ 87, 88, - 260, - 262, - 275, - 281, - 641, - 643, - 762, - 773, + 261, + 263, + 276, + 282, + 642, + 644, + 763, 774, 775, - 780 + 776, + 781 ], '/token/sso_token_provider.js': [ 60 diff --git a/scripts/translate-api b/scripts/translate-api index f18a617864..851e9eeea8 100755 --- a/scripts/translate-api +++ b/scripts/translate-api @@ -3,6 +3,7 @@ var fs = require('fs'); var Translator = require('./lib/translator'); var removeEventStreamOperations = require('./lib/remove-event-stream-ops').removeEventStreamOperations; +var setS3ExpiresString = require('./lib/set-s3-expires-string'); var util = require('util'); var path = require('path'); @@ -24,7 +25,7 @@ function ApiTranslator(basePath) { ApiTranslator.prototype.minimizeFile = function minimizeFile(filepath) { var opath = filepath.replace(/\.normal\.json$/, '.min.json'); var data = JSON.parse(fs.readFileSync(path.join(this._apisPath, filepath)).toString()); - var didModify = removeEventStreamOperations(data); + var didModify = removeEventStreamOperations(data) || setS3ExpiresString(data); if (didModify) { // original model modified, replace existing normal.json so docs/ts definitions are accurate fs.writeFileSync(path.join(this._apisPath, filepath), JSON.stringify(data, null, ' '));