Apologies if this is documented somewhere, but I've been trying to get this to work for the better part of two days, and haven't been able to.
I have a handler that, say, authenticates a user. I want it to return a status code of 401 (Unauthorized) if the user fails authentication, and 200 otherwise.
Here's how I'm trying to do it:
#[derive(Deserialize, Debug)]
struct LoginRequest {
password: String,
user: String,
}
#[post("/login", format="application/json", data="<login_request>")]
fn login<'r>(login_request: JSON<LoginRequest>) -> Result<status::Custom<JSON<String>>, Status> {
let authenticated;
match login_request.0.user.as_ref().map(String::as_ref) {
Some("foo") if login_request.0.password == "bar" => authenticated = true,
_ => authenticated = false,
}
if !authenticated {
return Err(Status::Unauthorized);
}
Ok(status::Custom(Status::Ok, JSON("authenticated")))
}
However, when I run this, and pass the wrong username and password, I get a 500 (ServerError), and I get the following in the logs:
POST /login application/json:
=> Matched: POST /login application/json
=> Error: Response was `Err`: Status { code: 401, reason: "Unauthorized" }.
=> Outcome: Failure
=> Warning: Responding with 500 Internal Server Error catcher.
=> Response succeeded.
I've looked through and ran all of the examples, but none of them seem to return an actual non-200 status code, even when they return errors.
I've even tried adding my own error handler for 401:
#[error(401)]
fn unauthorized(_: &Request) -> &'static str {
"{
\"errcode\" : \"401\",
\"error\" : \"Invalid user or password\"
}"
}
but got the same result.
Any help would be greatly appreciated!
Why your code doesn't work
The Status type isn't a Responder, so it can't produce a response itself. When you return a Result<T, E>, Rocket creates a response in one of two ways, depending on the type:
Result
where E: Debug If the Result is Ok, the wrapped responder is used to respond to the client. Otherwise, an Err with status 500 Internal Server Error is returned and the error is printed to the console using the Debug implementation.
Result
where E: Debug + Responder If the Result is Ok, the wrapped Ok responder is used to respond to the client. If the Result is Err, the wrapped Err responder is used to respond to the client.
This information is from the Responder documentation. As you can see, since E in your handler is Status, and Status doesn't implement Responder, you'll get the first functionality, which is why you see the 500.
How to return a particular status code
You can return a Failure if you want an error catcher to deal with a particular error code, or you can return a type from the status module if you want to customize the response. In your example, you could change the return type to status::Custom<...> (no Result) and change the Err case to status::Custom(Status::Unauthorized, ....) if you want to use a custom error response.
How to handle your problem better
The way you're handling login is subpar for several reasons. First and foremost, you're going to have a lot of code duplication if you want to do the login check in more than one handler. This will invariably lead to getting the check wrong somewhere or simply forgetting to do it. Second, at present, you have no easy way to, for example, redirect the user to a login page or something else if the login was stale or unsuccessful or what-have-you.
A better way to do what you're doing now is to create some new type, say ValidLogin, and implement FromData for that type. The FromData implementation can then call FromData on a JSON<LoginRequest> type, do the checking you're doing there, and if the check fails, return a Failure(Status::Unauthorized) (or whatever you want) or possibly a Forward, depending on the functionality you want. This will accomplish what you want in a type-safe, reusable, and dependable package. You can then use your ValidLogin type directly:
#[post("/login", format = "application/json", data = "<login>")]
fn login<'r>(login: ValidLogin) -> JSON<String> {
...
}
This is much cleaner, safer, and easier to read and write.
_P.S: The default status code is 200, so you should rarely need to set it yourself._
Thanks for the quick response!
Changing to using Result<status::Custom<JSON<String>>, Failure>, and returning Failure on error fixed the problem! Thanks!
I'll look into using FromData as well, but I wanted to start simple so I could better understand what was going on.
Thanks again!
I too was greatly strugglying to return a HTTP error code in certain cases. This helped.
It'd be awesome if more docs and examples could be written on this!
"Failure" ought to be mentioned in the discussion of default error catchers in https://rocket.rs/guide/responses/#errors, with an example, which could be as simple as this:
use rocket::http::Status;
use rocket::response::Failure;
fn command() -> Result<(), Failure> {
Err(Failure(Status::NotImplemented))
}
This is still fairly poorly documented.
This doesn't seem to work anymore @jebrosen can you please help me out. I want to return an error and change the https status.
fn delete_resource(conn: &db, req_resource_id: i32) -> Result<JsonValue, Status> {
match disable_resource(&conn, resource_id){
Ok(_)=> Ok(json!({"status": 204})),
Err(error) => Err(Status::NotModified),
}
}
This is what I got to work but I'd also like to attach the error with my status.
@Taha-Firoz I think you are looking for https://api.rocket.rs/v0.4/rocket/response/status/struct.Custom.html, which responds with both a status code and the specified Responder.
I was getting a pretty scary error when I did that, I'll post the exact error in a bit but it was about failed to Write buffer. It would compile and run but basically my handler was returning a result
@jebrosen This was the error I was getting:
Error: Failed to write response: Custom { kind: WriteZero, error: "failed to write whole buffer" }.
This is what I was returning from my handler:
Result<status::Custom<JsonValue>, Custom<String>>
But rn I did run the whole thing again and it seems to be working fine now, though I do have my logging get to normal whereas before I had logging set to critical.
Most helpful comment
This is still fairly poorly documented.