Search code examples
rustrust-axum

My Axum handler won't compile: the trait IntoResponse is not implemented


Totally new to Rust. I am trying to implement oauth authentication and I am using axum, with no success.. Here is my ugly code:

use axum::{
  Json,
  extract::Query,
  extract::Extension,
  http::StatusCode,
  response::IntoResponse
};
use serde_json::{json, Value};
use hyper;
use hyper_tls::HttpsConnector;
use hyper::header;
use cookie::Cookie;
use serde::{ Deserialize, Serialize };

#[derive(Clone)]
pub struct GitHubOAuth2 {
  client_id: String,
  redirect_uri: String,
  client_secret: String
}

#[derive(Serialize, Deserialize)]
pub struct CallbackAuthCode {
  code: String
}

impl GitHubOAuth2 {

  pub fn new(conf: String) -> GitHubOAuth2 {
    let json_content : Value = serde_json::from_str(&conf).expect("Invalid configuration.");
    GitHubOAuth2 {
      client_id: json_content["github_oauth2"]["client_id"].as_str().unwrap().to_string(),
      redirect_uri: json_content["github_oauth2"]["redirect_uri"].as_str().unwrap().to_string(),
      client_secret: json_content["github_oauth2"]["client_secret"].as_str().unwrap().to_string()
    }
  }
}

pub async fn callback(Query(params): Query<CallbackAuthCode>, Extension(conf): Extension<GitHubOAuth2>) -> impl IntoResponse {
  let params_code = &params.code;

  let mut get_token_url: String = String::from("https://www.github.com/login/oauth/access_token?client_id=");
  get_token_url.push_str(&conf.client_id);
  get_token_url.push_str("&redirect_uri=");
  get_token_url.push_str(&conf.redirect_uri);
  get_token_url.push_str("&client_secret=");
  get_token_url.push_str(&conf.client_secret);
  get_token_url.push_str("&code=");
  get_token_url.push_str(&params_code);
  println!("get_token_url: {}", get_token_url);

  let https = HttpsConnector::new();
  let client = hyper::Client::builder().build::<_, hyper::Body>(https);

  let req = hyper::Request::builder()
    .method(hyper::Method::POST)
    .uri(get_token_url)
    .header("Accept", "application/json")
    .body(hyper::Body::empty()).unwrap();

  match client.request(req).await {
    Ok(resp) => {
      println!("response: {}", resp.status());

      let redirectUri : String = resp.headers().get("Location").unwrap().to_str().unwrap().to_string();

      if resp.status() == 301 {
        let redirectReq = hyper::Request::builder()
          .method(hyper::Method::POST)
          .uri(redirectUri)
          .header("Accept", "application/json")
          .body(hyper::Body::empty()).unwrap();
          match client.request(redirectReq).await {
            Ok(mut redirectResp) => {
              let body = hyper::body::to_bytes(redirectResp.body_mut()).await.unwrap();
              println!("{} {:?}", redirectResp.status(), body);
              let body_as_json : Value = serde_json::from_slice(&body).unwrap();
              let bearer_token = body_as_json["access_token"].as_str().unwrap().to_string();
              let cookie = Cookie::build("hey", bearer_token).secure(true).http_only(true).finish();
              return (
                StatusCode::OK,
                [(header::SET_COOKIE, &cookie.value())],
                Json(json!({
                  "msg": "got that cookie"
                }))
              );
            },
            Err(mut redirect_e) => {
              return (
                StatusCode::INTERNAL_SERVER_ERROR,
                [(header::CONTENT_TYPE, &"application/json")],
                Json(json!({
                  "error": redirect_e.to_string()
                }))
              );
            }
          }
      } else {
        return (
          StatusCode::NOT_IMPLEMENTED,
          [(header::CONTENT_TYPE, &"application/json")],
          Json(json!({
            "error": String::from("service replies with unexpected response.")
          }))
        );
      }
    },
    Err(e) => {
      return (
        StatusCode::INTERNAL_SERVER_ERROR,
        [(header::CONTENT_TYPE, &"application/json")],
        Json(json!({
          "error": e.to_string()
        }))
      );
    }
  }
}

callback function aims to implement the 'callback' phase in oauth2, so it picks the auth code and uses it to call github IdP to collect authorization token. No big deal. Problem is this code does not compile and I don't get why. Compiler says:

error[E0277]: the trait bound `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>): IntoResponse` is not satisfied

callback function is attached to server as a get handler.
I started from the axum basic examples and tried to build my monster one step at time but now I am stuck. What am I missing?

My Cargo.toml:

[package]
name = "keymaster"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = "0.5.16"
axum-macros = "0.2.3"
hyper = { version = "0.14.20", features = ["full"] }
tokio = { version = "1.21.2", features = ["full"] }
tower = "0.4.13"
serde_json = "1.0.85"
serde = "1.0.145"
jwt-simple = "0.10"
clap = { version = "4.0.19", features = ["derive"] }
hyper-tls = "0.5.0"
cookie = "0.17.0"
follow-redirects = "0.1.3"
http = "0.2.9"
[dependencies.uuid]
version = "1.2.1"
features = [
    "v4",                # Lets you generate random UUIDs
    "fast-rng",          # Use a faster (but still sufficiently random) RNG
    "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]

20230409 update

Removing & from my &"application/json" causes the compiler to report some more errors. The full log is here:

--> src/server/handlers/github_oauth2.rs:14:5
  |
14 | use http::header::HeaderValue;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

error[E0308]: mismatched types
 --> src/server/handlers/github_oauth2.rs:92:41
  |
92 |                 [(header::CONTENT_TYPE, "application/json")],
  |                                         ^^^^^^^^^^^^^^^^^^ expected `&str`, found `str`
  |
  = note: expected reference `&&str`
             found reference `&'static str`
note: return type inferred to be `&&str` here
 --> src/server/handlers/github_oauth2.rs:81:22
  |
81 |                 return (
  |  ______________________^
82 | |                 StatusCode::OK,
83 | |                 [(header::SET_COOKIE, &cookie.value())],
84 | |                 Json(json!({
85 | |                   "msg": "got that cookie"
86 | |                 }))
87 | |               );
  | |_______________^

error[E0277]: the trait bound `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>): IntoResponse` is not satisfied
  --> src/server/handlers/github_oauth2.rs:40:126
   |
40  |   pub async fn callback(Query(params): Query<CallbackAuthCode>, Extension(conf): Extension<GitHubOAuth2>) -> impl IntoResponse {
   |  ______________________________________________________________________________________________________________________________^
41  | |   let params_code = &params.code;
42  | |
43  | |   let mut get_token_url: String = String::from("https://www.github.com/login/oauth/access_token?client_id=");
...   |
118 | |   }
119 | | }
   | |_^ the trait `IntoResponse` is not implemented for `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>)`
   |
   = help: the following other types implement trait `IntoResponse`:
             ()
             (Response<()>, R)
             (Response<()>, T1, R)
             (Response<()>, T1, T2, R)
             (Response<()>, T1, T2, T3, R)
             (Response<()>, T1, T2, T3, T4, R)
             (Response<()>, T1, T2, T3, T4, T5, R)
             (Response<()>, T1, T2, T3, T4, T5, T6, R)
           and 60 others

error[E0308]: mismatched types
  --> src/server/handlers/github_oauth2.rs:102:35
   |
102 |           [(header::CONTENT_TYPE, "application/json")],
   |                                   ^^^^^^^^^^^^^^^^^^ expected `&str`, found `str`
   |
   = note: expected reference `&&str`
              found reference `&'static str`
note: return type inferred to be `&&str` here
  --> src/server/handlers/github_oauth2.rs:81:22
   |
81  |                 return (
   |  ______________________^
82  | |                 StatusCode::OK,
83  | |                 [(header::SET_COOKIE, &cookie.value())],
84  | |                 Json(json!({
85  | |                   "msg": "got that cookie"
86  | |                 }))
87  | |               );
   | |_______________^

error[E0308]: mismatched types
  --> src/server/handlers/github_oauth2.rs:112:33
   |
112 |         [(header::CONTENT_TYPE, "application/json")],
   |                                 ^^^^^^^^^^^^^^^^^^ expected `&str`, found `str`
   |
   = note: expected reference `&&str`
              found reference `&'static str`
note: return type inferred to be `&&str` here
  --> src/server/handlers/github_oauth2.rs:81:22
   |
81  |                 return (
   |  ______________________^
82  | |                 StatusCode::OK,
83  | |                 [(header::SET_COOKIE, &cookie.value())],
84  | |                 Json(json!({
85  | |                   "msg": "got that cookie"
86  | |                 }))
87  | |               );
   | |_______________^

error[E0277]: the trait bound `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>): IntoResponse` is not satisfied
 --> src/server/handlers/github_oauth2.rs:40:108
  |
40 | pub async fn callback(Query(params): Query<CallbackAuthCode>, Extension(conf): Extension<GitHubOAuth2>) -> impl IntoResponse {
  |                                                                                                            ^^^^^^^^^^^^^^^^^ the trait `IntoResponse` is not implemented for `(StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>)`
  |
  = help: the following other types implement trait `IntoResponse`:
            ()
            (Response<()>, R)
            (Response<()>, T1, R)
            (Response<()>, T1, T2, R)
            (Response<()>, T1, T2, T3, R)
            (Response<()>, T1, T2, T3, T4, R)
            (Response<()>, T1, T2, T3, T4, T5, R)
            (Response<()>, T1, T2, T3, T4, T5, T6, R)
          and 60 others

Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.
warning: `keymaster` (bin "keymaster") generated 1 warning
error: could not compile `keymaster` due to 5 previous errors; 1 warning emitted

Solution

  • The error seems clear? It's telling you that

    (StatusCode, [(HeaderName, &&str); 1], Json<serde_json::Value>)
    

    does not implement IntoResponse.

    Since this is a 3-uple and the first member is a StatusCode, the closest impl would be

    impl<R, T1> IntoResponse for (StatusCode, T1, R)
    where
        T1: IntoResponseParts,
        R: IntoResponse,
    

    There is an

    impl<T> IntoResponse for Json<T> where T: Serialize
    

    and serde_value::Value implements Serialize, which leaves the middle item:

    [(HeaderName, &&str); 1]
    

    This is an array of a (k, v) tuple so

    impl<K, V, const N: usize> IntoResponse for [(K, V); N]
    where
        K: TryInto<HeaderName>,
        <K as TryInto<HeaderName>>::Error: Display,
        V: TryInto<HeaderValue>,
        <V as TryInto<HeaderValue>>::Error: Display
    

    is the only candidate. HeaderName is trivially convertible to a HeaderName, but what about &&str as TryInto<HeaderValue>? Looking at impl TryFrom<> for HeaderValue (which is the mirror and usually implemented trait) we can see:

    impl<'a> TryFrom<&'a [u8]> for HeaderValue
    impl<'a> TryFrom<&'a String> for HeaderValue
    impl<'a> TryFrom<&'a str> for HeaderValue
    impl TryFrom<String> for HeaderValue
    impl TryFrom<Vec<u8, Global>> for HeaderValue
    

    And indeed there is no implementation from &&str, so &&str can't be converted to a HeaderValue.

    Just remove the completely unnecessary referencing of you string literals, a string literal is already an &'static str.