Beginner struggling with form POST using actix_web

Hello,

I am relearning Rust and programming a bit, so I am not sure what I am doing wrong and my troubleshooting has just brought me in circles.

I haven't seen a lot of examples using actix_web that also include front end code, so I'm not sure if I'm just not structuring things properly on the front end, if I'm failing on the back end, or both.

Any help would be appreciated.

The basic webpage:

<form action="/form" id="form" method="POST">
    <div>
        <label for="username">Username:</label>
        <input name="username" id="username" />
    </div>
    <div>
        <label for="test">Test:</label>
        <input name="test" id="test" />
    </div>
    <div>
        <button id="submitButton">Submit</button>
    </div>
</form>
</body>
<script>
    document.addEventListener('DOMContentLoaded', function() {
        const send = document.querySelector('form')

        send.addEventListener("submit", async (e) => {
            e.preventDefault();
            var formData = new FormData(document.querySelector("#form"));
            for (var [key, value] of formData.entries()) {
                console.log(key, value);
            }
            const response = await fetch("/form", {method: "POST", body: formData });
            console.log(await response.text());
        });
    });
</script>

Snippets of the rust code:

#[derive(Deserialize)]
struct Info {
    username: String,
    test: String,
}
#[post("/form")]
async fn form(form: web::Json<Info>) -> HttpResponse {
    HttpResponse::Ok().body(format!("Welcome from the form {} with test: {}!", form.username, form.test))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        //let cors = Cors::default().allow_any_origin().send_wildcard();
        //App::new().wrap(cors)
        App::new()
            .service(fs::Files::new("/", "./static").show_files_listing())
            .service(form)
    })
        .bind(("127.0.0.1", 3000))?
        .run()
        .await
}

My attempt at sending a request with curl. I have gotten GET to work easily on simple examples but not POST with a form, or GET with a form, which seems like it would require destructuring the form inputs manually.

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"username":"xyz","test":"xyz"}' \
  http://127.0.0.1:3000/form
Request did not meet this resource's requirements.%   

In the handler for the /form endpoint in Actix web you are expecting a JSON payload, but you're sending FormData.

1 Like

Ok thank you, that cleared the curl error.

Something is still wrong with the front end request

Console:

index.html:37 
 POST http://127.0.0.1:3000/form 405 (Method Not Allowed)
index.html:38 Request did not meet this resource's requirements.

You need to do the same to fix the request sent from the browser: Add the content-type header and send a JSON payload.

I'm still scratching my head on this one. I tried a few variations of modifying the endpoint but it still doesn't seem to work.

I tried both json and form as the content-type for the endpoint and for the front end.

 document.addEventListener('DOMContentLoaded', function() {
        const send = document.querySelector('form')

        send.addEventListener("submit", async (e) => {
            e.preventDefault();
            var formData = new FormData(document.querySelector("#form"));
            for (var [key, value] of formData.entries()) {
                console.log(key, value);
            }
            const response = await fetch("/form", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: formData
            });
            console.log(await response.text());
        });
    });

with corresponding Rust endpoint:

#[post("/form")]
async fn form(form: web::Json<Info>) -> HttpResponse {
    HttpResponse::Ok().body(format!("Welcome from the form {} with test: {}!", form.username, form.test))
}

Console result:

index.html:37  POST http://127.0.0.1:3000/form 405 (Method Not Allowed)
(anonymous) @ index.html:37
index.html:44 Request did not meet this resource's requirements.
    document.addEventListener('DOMContentLoaded', function() {
        const send = document.querySelector('form')

        send.addEventListener("submit", async (e) => {
            e.preventDefault();
            var formData = new FormData(document.querySelector("#form"));
            for (var [key, value] of formData.entries()) {
                console.log(key, value);
            }
            const response = await fetch("/form", {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                body: formData
            });
            console.log(await response.text());
        });
    });


#[post("/form")]
async fn form(form: web::Form<Info>) -> HttpResponse {
    HttpResponse::Ok().body(format!("Welcome from the form {} with test: {}!", form.username, form.test))
}

Console result:

index.html:37  POST http://127.0.0.1:3000/form 405 (Method Not Allowed)
(anonymous) @ index.html:37
index.html:44 Request did not meet this resource's requirements.

Keeping the rust the same but modifying the javascript:

const response = await fetch("/form", {
                method: "POST",
                headers: {
                    "Content-Type": "multipart/form-data",
                },
                body: formData
            });

Still the same console result:


index.html:37  POST http://127.0.0.1:3000/form 405 (Method Not Allowed)
(anonymous) @ index.html:37
index.html:44 Request did not meet this resource's requirements.

If your Rust backend uses web::Json then your JS frontend should use JSON.stringify to serialize the body and send "application/json" in the header.

If your Rust backend uses web::Form then your JS frontend should send a URLSearchParams in the body and "application/x-www-form-urlencoded" in the header.

Neither case should send a FormData in the body.

1 Like

I have no idea about actix, however my web experience tells that you probably should use PUT method instead of POST.

This depends on the semantic of the request (which were not described by OP), most importantly whether or not this request should be idempotent.

In this case it does not matter. As other already replied the problem lies with actix extractors requiring specific request format/encoding. actix would behave the same if form service would be marked as PUT request handler.

Aha! I had a few things wrong.

As for PUT vs PUSH, I think the intent here is closer to PUSH. I am working on building a small API and this was me just trying to get the very basic part working first before I use a real struct.

First thing, I had to change a few things in the Rust code. I changed the form function to implement Responder.
I also had to fix the issue when I set up the HttpServer where I was using "service" instead of "route". I also had to change the order of the call so that my static file service was after the route for the form.

Doing these things let me successfully send a curl POST request, and then edit the front end to properly generate the javascript code. I found an example here which made me realize most of the issues: https://towardsdev.com/building-high-performance-rest-apis-with-actix-web-or-axum-in-rust-34c25ea8a263

#[derive(Deserialize)]
struct Info {
    username: String,
    test: String,
}

async fn form(form: web::Json<Info>) -> impl Responder {
    web::Json(format!("Welcome from the form {} with test: {}!", form.username, form.test))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/form", web::post().to(form))
            .service(fs::Files::new("/", "./static").show_files_listing())
    })
        .bind(("127.0.0.1", 3000))?
        .run()
        .await
}