最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Form submission triggers CORs error as fetch - Stack Overflow

programmeradmin4浏览0评论

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"
发布评论

评论列表(0)

  1. 暂无评论