This is not a duplicate of Why does my JavaScript code receive a "No 'Access-Control-Allow-Origin' header is present on the requested resource" error, while Postman does not? because none of the answers to that question fix this issue. Many of the answers to that question just describes CORs and many of the answers might as well be copy and pasted from Wikipedia and do not help at all. I am not asking for a description of CORs or how to generally enable it, I am asking about a specific technical issue with a minimal reproducible example
I am attempting to upload some data to S3 using a pre-signed post request. In doing this I have hit a CORs error:
Access to fetch at 'http://localhost:4566/17acvaclgm-pictures' from origin 'http://127.0.0.1:4006' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I wrote a simple example doing the upload with a form:
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>HTML 5 Boilerplate</title>
</head>
<body>
<form
action="http://localhost:4566/17acvaclgm-pictures"
method="post"
enctype="multipart/form-data"
>
<input type="hidden" name="key" value="browserObject" />
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
</body>
</html>
This worked returning:
HTTP/1.1 204 NO CONTENT
Server: TwistedWeb/24.3.0
Date: Sun, 26 Jan 2025 05:53:45 GMT
Access-Control-Allow-Origin: *
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Access-Control-Allow-Methods: GET, HEAD, PUT, POST, DELETE
Location: :4566/browserObject
ETag: "f520dae5b605d9c92072c67d1ae2b8a7"
x-amz-server-side-encryption: AES256
x-amz-request-id: f5719cde-db04-498a-b8e3-d13a46f11e41
x-amz-id-2: s9lzHYrFp76ZVxRcpX9+5cjAnEH2ROuNkd2BHfIa6UkFVdtjf5mKR3/eTPFvsiP/XV/VLi31234=
So to attempt to fix my attempt I clicked Copy as fetch
giving:
fetch("http://localhost:4566/17acvaclgm-pictures", {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
"cache-control": "max-age=0",
"content-type": "multipart/form-data; boundary=----WebKitFormBoundary9m2KrDP2o5TzQkvt",
"sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "cross-site",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1"
},
"referrer": "http://127.0.0.1:4040/",
"referrerPolicy": "strict-origin-when-cross-origin",
"method": "POST",
"mode": "cors",
"credentials": "omit"
});
I then updated referrer
and added the body
with my form data:
let formData = new FormData();
// ...
fetch("http://localhost:4566/17acvaclgm-pictures", {
"headers": {
// ...
},
// ...
referrer: "http://127.0.0.1:4006/"
// ...
body: formData
});
I then ran this, and received the same error.
I checked the actual request headers and found despite copying that the request from fetch
had different headers:
accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
accept-language:
en-GB,en-US;q=0.9,en;q=0.8
cache-control:
max-age=0
content-type:
multipart/form-data; boundary=----WebKitFormBoundaryIjvYSjxNxnzfxn11
referer:
http://127.0.0.1:4006/
sec-ch-ua:
"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
sec-ch-ua-mobile:
?0
sec-ch-ua-platform:
"Windows"
upgrade-insecure-requests:
1
user-agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
As opposed to the headers in the successful request:
accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
accept-encoding:
gzip, deflate, br, zstd
accept-language:
en-GB,en-US;q=0.9,en;q=0.8
cache-control:
max-age=0
connection:
keep-alive
content-length:
335341
content-type:
multipart/form-data; boundary=----WebKitFormBoundary9m2KrDP2o5TzQkvt
host:
localhost:4566
origin:
http://127.0.0.1:4040
referer:
http://127.0.0.1:4040/
sec-ch-ua:
"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
sec-ch-ua-mobile:
?0
sec-ch-ua-platform:
"Windows"
sec-fetch-dest:
document
sec-fetch-mode:
navigate
sec-fetch-site:
cross-site
sec-fetch-user:
?1
upgrade-insecure-requests:
1
user-agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Notably here Origin
is important to S3 as noted on .html and by . So I attempt to add this:
// ...
fetch("http://localhost:4566/17acvaclgm-pictures", {
"headers": {
// ...
},
// ...
origin: "http://127.0.0.1:4006"
});
This also failed with the same CORs error.
What am I doing wrong here?
All this can be tested locally by running localstack start -d
then settings the CORs settings then running the fetch and/or submitting the form.
Here is some Rust code which sets the CORs settings then starts a server serving the form:
use axum::http::header;
use axum::{routing::get, Router};
use s3::serde_types::{CorsConfiguration, CorsRule};
use s3::Bucket;
use std::process::Command;
use std::process::Stdio;
use std::time::Duration;
const BUCKET_NAME: &str = "17acvaclgm-pictures"; // bucket name
const MAX_SIZE: u32 = 4194304; // 4mb
#[tokio::main]
async fn main() {
// Include object data.
let data = include_bytes!("picture.webp");
// Wait for localstack instance to be running.
let _wait = Command::new("localstack")
.args(["wait", "-t", &10u32.to_string()])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
// Set AWS region.
let region = s3::Region::Custom {
region: String::from("us-east-1"),
endpoint: String::from("http://localhost:4566"),
};
println!("region: {region:?}");
// Set AWS credentials.
let credentials = awscreds::Credentials {
access_key: Some(String::from("test")),
secret_key: Some(String::from("test")),
security_token: None,
session_token: None,
expiration: None,
};
println!("credentials: {credentials:?}");
// Create S3 bucket.
let _create_bucket_response = s3::Bucket::create_with_path_style(
BUCKET_NAME,
region.clone(),
credentials.clone(),
s3::BucketConfiguration::public(),
)
.await
.unwrap();
let s3_bucket = s3::Bucket::new(BUCKET_NAME, region, credentials)
.unwrap()
.with_path_style();
// Set permissive CORs configuration.
let account_id = String::from("000000000000");
let _cors_response = s3_bucket
.put_bucket_cors(
&account_id,
&CorsConfiguration::new(vec![CorsRule::new(
Some(vec![String::from("*")]),
vec![
String::from("GET"),
String::from("HEAD"),
String::from("PUT"),
String::from("POST"),
String::from("DELETE"),
],
vec![String::from("*")],
None,
None,
None,
)]),
)
.await
.unwrap();
// Upload object using pre-signed url outside browser.
let name = "nonBrowserObject";
let url = presign(name, s3_bucket.clone()).await;
println!("url: {url}\nkey: {name}");
upload(name, &url, data).await;
// Upload object using pre-signed url in browser by starting a server with a form upload using the pre-signed url.
let name = "browserObject";
let url = presign(name, s3_bucket.clone()).await;
println!("url: {url}\nkey: {name}");
let index = format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HTML 5 Boilerplate</title>
</head>
<body>
<form action="http://localhost:4566/{BUCKET_NAME}" method="post" enctype="multipart/form-data">
<input type="hidden" name="key" value="{name}" />
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
</body>
</html>
"#,
);
let app = Router::new().route(
"/",
get(|| async {
axum::response::Response::builder()
.status(axum::http::StatusCode::OK)
.header(
header::CONTENT_TYPE,
header::HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
)
.body(axum::body::Body::from(index))
.unwrap()
}),
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:4040").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn presign(name: &str, s3_bucket: Box<Bucket>) -> String {
let post_policy = s3::PostPolicy::new(s3::post_policy::PostPolicyExpiration::ExpiresIn(60))
.condition(
s3::PostPolicyField::Key,
s3::PostPolicyValue::Exact(std::borrow::Cow::from(name.to_owned())),
)
.unwrap()
.condition(
s3::PostPolicyField::Bucket,
s3::PostPolicyValue::Exact(std::borrow::Cow::from(s3_bucket.name())),
)
.unwrap()
.condition(
s3::PostPolicyField::ContentLengthRange,
s3::PostPolicyValue::Range(0, MAX_SIZE),
)
.unwrap();
let post = s3_bucket.presign_post(post_policy).await.unwrap();
post.url
}
async fn upload(name: &str, url: &str, data: &[u8]) {
tokio::time::sleep(Duration::from_secs(10)).await;
let part = reqwest::multipart::Part::bytes(Vec::from(data))
.file_name(name.to_owned())
.mime_str("image/webp")
.unwrap();
let form = reqwest::multipart::Form::new()
.text("key", name.to_owned())
.text("bucket", BUCKET_NAME.to_string())
.part("file", part);
let response = reqwest::Client::new()
.post(url)
.multipart(form)
.send()
.await
.unwrap();
assert!(
response.status().is_success(),
"{}, {:?}",
response.status(),
response
.text()
.await
.map(|s| s.chars().take(300).collect::<String>())
);
}
where Cargo.toml
is:
[package]
name = "testing"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.42.0", features = ["full"] }
rust-s3 = { git=";, rev="35dcc91893ce3376b94c6e2e7e029afe43ea9e7c"}
aws-creds = "0.38.0"
reqwest = { version = "0.12.7", features = ["cookies", "json", "stream", "multipart"] }
axum = "0.8.1"
mime = "0.3.17"