LemmyNet / activitypub-federation-rust

High-level Rust library for the Activitypub protocol
GNU Affero General Public License v3.0
409 stars 46 forks source link

Can't figure out how to return a "not found" response #113

Closed ninjadev64 closed 1 month ago

ninjadev64 commented 3 months ago

I've been following the docs.rs "tutorial" for this library (leaning extensively on the examples in this repo as the "tutorial" is quite incomplete) and I have no idea how to return a "not found" response from my http_get_user handler. I'm using rocksdb btw.

pub async fn http_get_user(_header_map: HeaderMap, Path(name): Path<String>, db: Data<ArcDB>) -> impl IntoResponse {
    let db_user: DbUser = match db.get_cf(&db.cf_handle("users").unwrap(), &name).unwrap() {
        Some(bytes) => serde_json::from_slice(&bytes).unwrap(),
        None => unimplemented!(), // return Not Found here how???
    };
    let json_user = db_user.into_json(&db).await.unwrap();
    FederationJson(WithContext::new_default(json_user)).into_response()
}

I can't seem to return my own (StatusCode::NOT_FOUND, "not found")-esque thing because it's a different type to FederationJson.into_response(self)'s return type.

Neither can I find anything in this library to pass to FederationJson() or WithContext::new_default() that would indicate a "not found" status.

Could you give me some guidance on implementing this?

ninjadev64 commented 3 months ago

Forgot to mention that I tried using axum_macros's debug_handler like the example does, but it gives some pretty bad generic trait bound errors for both Path<String> and Data<ArcDB>.

ninjadev64 commented 3 months ago

I was using a version of axum_macros that was incompatible with my installed version of axum. I've solved the original problem like this:

#[debug_handler]
pub async fn http_get_user(_header_map: HeaderMap, Path(name): Path<String>, db: Data<ArcDB>) -> Result<impl IntoResponse, impl IntoResponse> {
    let db_user: DbUser = match db.get_cf(&db.cf_handle("users").unwrap(), &name).unwrap() {
        Some(bytes) => serde_json::from_slice(&bytes).unwrap(),
        None => return Err((StatusCode::NOT_FOUND, format!("User {name} not found"))),
    };
    let json_user = db_user.into_json(&db).await.unwrap();
    Ok(FederationJson(WithContext::new_default(json_user)).into_response())
}

However, when I try to check the content type of the request and return an HTML representation if it's not ActivityPub, I get this:

#[debug_handler]
pub async fn http_get_user(header_map: HeaderMap, Path(name): Path<String>, db: Data<ArcDB>) -> Result<Box<dyn IntoResponse>, impl IntoResponse> {
    let db_user: DbUser = match db.get_cf(&db.cf_handle("users").unwrap(), &name).unwrap() {
        Some(bytes) => serde_json::from_slice(&bytes).unwrap(),
        None => return Err((StatusCode::NOT_FOUND, format!("User {name} not found"))),
    };
    let accept = header_map.get("accept").map(|v| v.to_str().unwrap());
    if accept == Some(FEDERATION_CONTENT_TYPE) {
        let json_user = db_user.into_json(&db).await.unwrap();
        Ok(Box::new(FederationJson(WithContext::new_default(json_user)).into_response()))
    } else {
        Ok(Box::new((StatusCode::OK, format!("HTML repr of {}", db_user.display_name))))
    }
}
error[E0277]: the trait bound `Box<dyn IntoResponse>: IntoResponse` is not satisfied
   --> src/users.rs:138:97
    |
138 | pub async fn http_get_user(header_map: HeaderMap, Path(name): Path<String>, db: Data<ArcDB>) -> Result<Box<dyn IntoResponse>, impl IntoResponse> {
    |                                                                                                 ^^^^^^ the trait `IntoResponse` is not implemented for `Box<dyn IntoResponse>`, which is required by `Result<Box<dyn IntoResponse>, impl IntoResponse>: IntoResponse`
    |
    = help: the following other types implement trait `IntoResponse`:
              axum::body::Empty<axum::body::Bytes>
              http_body::combinators::box_body::BoxBody<axum::body::Bytes, E>
              http_body::combinators::box_body::UnsyncBoxBody<axum::body::Bytes, E>
              axum::body::Bytes
              axum::extract::rejection::FailedToBufferBody
              axum::extract::rejection::LengthLimitError
              axum::body::Full<axum::body::Bytes>
              axum::extract::rejection::UnknownBodyError
            and 126 others
    = note: required for `Result<Box<dyn IntoResponse>, impl IntoResponse>` to implement `IntoResponse`
note: required by a bound in `__axum_macros_check_http_get_user_into_response::{closure#0}::check`
   --> src/users.rs:138:97
    |
138 | pub async fn http_get_user(header_map: HeaderMap, Path(name): Path<String>, db: Data<ArcDB>) -> Result<Box<dyn IntoResponse>, impl IntoResponse> {
    |                                                                                                 ^^^^^^ required by this bound in `check`
Tangel commented 3 months ago

Hi! Perhaps you could wrap the different types in an enum. The following example code may help you.

enum Resp<T> where T: Serialize {
    ActivityPub(FederationJson<T>),
    Axum((StatusCode, String)),
}

impl<T> axum::response::IntoResponse for Resp<T> where T: Serialize {
    fn into_response(self) -> axum::response::Response {
        match self {
            Self::ActivityPub(data) => data.into_response(),
            Self::Axum(data) => data.into_response(),
        }
    }
}

async fn http_get_user() -> Result<Resp, impl IntoResponse> {
    // snip
    if accept == Some(FEDERATION_CONTENT_TYPE) {
        let json_user = db_user.into_json(&db).await.unwrap();
        Ok(Resp::ActivityPub(FederationJson(WithContext::new_default(json_user)))
    } else {
        Ok(Resp::Axum((StatusCode::OK, format!("HTML repr of {}", db_user.display_name))))
    }
}
ninjadev64 commented 3 months ago

Oh, that's great, thank you!

Asudox commented 1 month ago

You can use Result as the return type.