actix / actix-web

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
https://actix.rs
Apache License 2.0
21.6k stars 1.67k forks source link

Allow different form submits on the same resource / Invalid route fails ungracefully #568

Open mbaeten opened 6 years ago

mbaeten commented 6 years ago

Hey, first I want to thank you for your great work. Maybe there's a simple solution to this that i missed.

  1. I would like to handle multiple/different forms on the same URL.

  2. If you submit form 2, it seems that the first POST resource (form1) is chosen and then fails ungracefully to execute (because struct Form1 does not match the POST data). If a route is chosen, it should always be able to execute it, don't you think so, too?

  3. Is there a way to forward all POST data to the called function so that I can handle it on my own?

Relevance: If parts of the Form depend on the user's selections (dynamically created Form fields), you cannot know beforehand which data will be sent to the server.

Example:

extern crate actix_web;

#[macro_use]
extern crate serde_derive;

use actix_web::{
    http, middleware, server, App, Error, Form, HttpRequest, HttpResponse, Result, State,
};

struct AppState {}

#[derive(Deserialize)]
pub struct Form1 {
    test1: String,
}
#[derive(Deserialize)]
pub struct Form2 {
    test2: String,
}

fn get(_state: State<AppState>) -> Result<HttpResponse, Error> {
    //println!("index::get.state = {}", _state);
    Ok(HttpResponse::build(http::StatusCode::OK)
        .content_type("text/html")
        .body(
            "Form1: <form method=\"post\" action=\"/\">
<input type=\"text\" name=\"test1\">
<input type=\"submit\" value=\"Submit\">
</form>
<br>
Form2: <form method=\"post\" action=\"/\">
<input type=\"text\" name=\"test2\">
<input type=\"submit\" value=\"Submit\">
</form>",
        ))
}

fn post_form1((req, form): (HttpRequest<AppState>, Form<Form1>)) -> Result<HttpResponse> {
    println!("Handling form1: {:?}", req);
    Ok(HttpResponse::build(http::StatusCode::OK)
        .content_type("text/html")
        .body(format!("Form1.test1 =  {}", form.test1)))
}

fn post_form2((req, form): (HttpRequest<AppState>, Form<Form2>)) -> Result<HttpResponse> {
    println!("Handling form2: {:?}", req);
    Ok(HttpResponse::build(http::StatusCode::OK)
        .content_type("text/html")
        .body(format!("Form2.test2 = {}", form.test2)))
}

fn error_404(_state: State<AppState>) -> Result<HttpResponse> {
    Ok(HttpResponse::build(http::StatusCode::OK)
        .content_type("text/plain")
        .body(format!("Eww! That's a 404.")))
}

fn main() {
    server::new(|| {
        App::with_state(AppState {})
            // enable logger
            .middleware(middleware::Logger::default())
            .resource("/", |r| {
                r.method(http::Method::GET).with(get);

                // works properly unless you use form2 (which results in a blank page)
                r.method(http::Method::POST).with(post_form1);

                // will never be called
                r.method(http::Method::POST).with(post_form2);
            })
            .default_resource(|r| r.with(error_404))
    })
    .bind("127.0.0.1:8080")
    .unwrap()
    .run();
}
DoumanAsh commented 5 years ago

I'm not sure there is good way in serde to allow it, but maybe you could try untagged union of two forms? https://serde.rs/enum-representations.html#untagged

Other than that you should be able to get body of request by accepting String https://actix.rs/actix-web/actix_web/trait.FromRequest.html#impl-FromRequest%3CS%3E-1 or bytes::Bytes for raw bytes

fafhrd91 commented 5 years ago

I am actually think this idea might make sense

DoumanAsh commented 5 years ago

I think it can be resolved by serde alone though? 😄

fafhrd91 commented 5 years ago

Idea is to be able to create multiple handlers for matched route, with different set of extractors