Your CORS configuration on the S3 bucket looks fine for the scenario you describe.
I believe the problem is with the endpoint URL that is resulting from your code. I had a similar problem and it was confusing and rather difficult to track down.
For some reason the AWS SDK seems to generate the endpoint URL differently depending on the method you use to set the region information and/or the bucket name. When the resulting URL doesn't contain region information (which yours does not) it causes the pre-flight request to fail, which results in a misleading error message in the brower's console about CORS (which can sometimes be the source of a preflight failure).
'Problem' endpoint format: http(s)://<bucketname>.s3.amazonaws.com/<key>
'Desired' endpoint format: http(s)://s3-<region>.amazonaws.com/<bucketname>/<key>
Try using the component I've provided here to validate your AWS S3 configuration, access and CORS settings. Then, you can easily extract out the S3 specific stuff in to a service if you choose.
Here are the steps:
- Confirm you are running the current version of the AWS SDK (2.10.0 as of 2017-02-08). Just in case you aren't familiar with how to check this, open /node_modules/aws-sdk/dist/aws-sdk.js and look at the comment at the top of the file to determine the version.
- Add the component below to your project. I have successfully tested this against my own S3 instance.
- Make sure you update the configuration values and specifically specify the region that your S3 bucket is in. If you are unsure you can find this using the AWS console. Of course you also need to replace the other config values using your credentials and S3 bucket name.
- Configure systemjs for the AWS SDK library (see system.config.js below)
- Add the component to your module's declarations (see app.module.ts below)
- Reference the component (
<s3-upload-test></s3-upload-test>
) in the template for AppComponent (see app.component.ts below).
s3-upload-test.component.ts:
import { Component } from '@angular/core';
import { Credentials, S3 } from 'aws-sdk';
@Component({
selector: 's3-upload-test',
template: `
<div class="uploadSection">
<hr>
<h3>S3 File Upload Test</h3>
<div class="subsection">
<h4>Confirm Endpoint Format:</h4>
<div class="indent">
The endpoint should be in the following format <span class="monospace">s3-<region>.amazonaws.com</span>.
<pre>
Based on the configuration information you provided:
Expect Endpoint: {{expectEndpoint}}
Actual Endpoint: {{actualEndpoint}}
</pre>
</div>
</div>
<div class="subsection">
<h4>Select File:</h4>
<div class="indent">
<input type="file" (change)="fileEvent($event)" />
</div>
</div>
<div class="subsection">
<h4>Upload Status/Results:</h4>
<div class="indent">
<span class="monospace result">{{uploadStatus}}</span>
</div>
</div>
<hr>
</div>
`,
styles: [`
.uploadSection { font-family: sans-serif; }
.monospace { font-family: monospace; }
.subsection { margin-top: 35px;}
.indent { margin-left: 20px;}
.result { background-color: lightyellow }
`]
})
export class S3UploadTestComponent {
// Replace the values with your own
private readonly _awsConfig = {
accessKeyId: "<your keyId>",
secretAccessKey: "<your secret>",
s3BucketRegion: "<your region>", // example: "us-west-2"
s3BucketName: "<your bucket>" // example: "mycompany.testbucket"
}
private _awsCredentials: Credentials;
private _s3ClientConfig: S3.ClientConfiguration;
private _s3: S3;
uploadStatus: string = "(no upload yet)";
expectEndpoint: string;
actualEndpoint: string;
constructor() {
// Create an AWS S3 client
this._awsCredentials = new Credentials(this._awsConfig.accessKeyId, this._awsConfig.secretAccessKey);
this._s3ClientConfig = {
credentials: this._awsCredentials,
region: this._awsConfig.s3BucketRegion,
sslEnabled: true
};
this._s3 = new S3(this._s3ClientConfig);
// Set the expected and actual endpoints
var isRegionUSEast :boolean = (this._awsConfig.s3BucketRegion).toLowerCase() == "us-east-1";
var endpointHost :string = isRegionUSEast ? "s3" : "s3-" + this._awsConfig.s3BucketRegion
this.expectEndpoint = endpointHost + ".amazonaws.com";
this.actualEndpoint = this._s3.config.endpoint;
}
// Event triggered when a file has been specified
fileEvent(fileInput: any) {
this.uploadStatus = "starting upload...";
// get the file to upload
let file: File = fileInput.target.files[0];
console.log(file);
// upload file to S3
let putObjectRequest: S3.PutObjectRequest = {
Key: 'categories/' + file.name,
Body: file,
Bucket: this._awsConfig.s3BucketName,
ContentType: file.type,
ServerSideEncryption: "AES256"
};
// use "that" to be able to reach component properties within the then/catch callback functions
let that = this;
// upload to S3
this._s3.upload(putObjectRequest).promise()
.then(function (response: S3.ManagedUpload.SendData) {
that.uploadStatus = "Success!\n File URI: " + response.Location;
// alert("upload successful!");
})
.catch(function (err: Error) {
var errMsg = "";
errMsg += "upload failed.\n ";
errMsg += "Error Message: " + err.message + "\n ";
errMsg += "NOTE: an error message of 'Network Failure' may mean that you have the wrong region or the wrong bucket name.";
that.uploadStatus = errMsg;
// alert(errMsg);
});
}
}
systemjs.config.js additions:
(function (global) {
System.config({
...
map: {
...
'aws-sdk': 'npm:aws-sdk'
},
packages: {
...
'aws-sdk': {
defaultExtension: 'js',
main: 'dist/aws-sdk.js',
format: 'global'
}
}
});
})(this);
app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { S3UploadTestComponent } from './s3-upload-test.component';
@NgModule({
imports: [BrowserModule],
declarations: [
AppComponent,
S3UploadTestComponent,
],
bootstrap: [AppComponent]
})
export class AppModule { }
app.component.ts:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>Hello {{name}}</h1>
<s3-upload-test></s3-upload-test>
`,
})
export class AppComponent { name = 'Angular'; }
AWS S3 bucket CORS Configuration:
NOTE: you may want to make your more restrictive, as appropriate for your security needs
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>DELETE</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
AWS IAM Policy (attach to user or group):
NOTE: you will almost certainly want to make the allowed actions more restrictive, as appropriate for your security needs
NOTE: replace <your bucketname>
with appropriate value
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1485926968000",
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::<your bucketname>/*"
]
}
]
}
If this doesn't solve your problem, use Chrome dev tools and look at the 'Network' tab to see the OPTIONS request to the S3 API and update your question with the entire response. When AWS S3 pre-flights fail, they usually provide good information in the response.