1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
//! This example demonstrates how to use any http Client
//! layer stack in a high level manner using the HttpClientExt.
//!
//! ```sh
//! cargo run --example http_high_level_client --features=compression,http-full
//! ```
//!
//! # Expected output
//!
//! You should see the output printed and the example should exit with a success status code.
//! In your logs you will also find each request traced twice, once for the client and once for the server.
// rama provides everything out of the box to build a complete web service.
use rama::{
Context, Layer, Service,
http::{
Body, BodyExtractExt, Request, Response, StatusCode,
client::EasyHttpWebClient,
headers::{Accept, Authorization, HeaderMapExt},
layer::{
auth::{AddAuthorizationLayer, AsyncRequireAuthorizationLayer},
compression::CompressionLayer,
decompression::DecompressionLayer,
retry::{ManagedPolicy, RetryLayer},
trace::TraceLayer,
},
server::HttpServer,
service::client::HttpClientExt,
service::web::WebService,
service::web::response::{IntoResponse, Json},
},
net::user::Basic,
rt::Executor,
utils::{backoff::ExponentialBackoff, rng::HasherRng},
};
// Everything else we need is provided by the standard library, community crates or tokio.
use serde_json::json;
use std::time::Duration;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, fmt};
const ADDRESS: &str = "127.0.0.1:62004";
#[tokio::main]
async fn main() {
setup_tracing();
tokio::spawn(run_server(ADDRESS));
// Thanks to the import of [`rama::http::client::HttpClientExt`] we can now also
// use the high level API for this service stack.
//
// E.g. `::post(<uri>).header(k, v).form(<data>).send().await?`
let client = (
TraceLayer::new_for_http(),
DecompressionLayer::new(),
// you can try to change these credentials or omit them completely,
// to see the unauthorized responses, in other words: see the auth middleware in action
//
// NOTE: the high level http client has also a `::basic` method
// that can be used to add basic auth headers only for that specific request
AddAuthorizationLayer::basic("john", "123")
.as_sensitive(true)
.if_not_present(true),
RetryLayer::new(
ManagedPolicy::default().with_backoff(
ExponentialBackoff::new(
Duration::from_millis(100),
Duration::from_secs(30),
0.01,
HasherRng::default,
)
.unwrap(),
),
),
)
.into_layer(EasyHttpWebClient::default());
//--------------------------------------------------------------------------------
// Low Level (Regular) http client (stack) service example.
// It does make use of the `BodyExtractExt` trait to extract the body as string.
//--------------------------------------------------------------------------------
let resp = client
.serve(
Context::default(),
Request::builder()
.uri(format!("http://{ADDRESS}/"))
.method("GET")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let body = resp.try_into_string().await.unwrap();
tracing::info!("body: {:?}", body);
assert_eq!(body, "Hello, World!");
//--------------------------------------------------------------------------------
// The examples below are high level http client examples
// using the `HttpClientExt` trait.
//--------------------------------------------------------------------------------
// Get Json Response Example
#[derive(Debug, serde::Deserialize)]
struct Info {
name: String,
example: String,
magic: u64,
}
let info: Info = client
.get(format!("http://{ADDRESS}/info"))
.header("x-magic", "42")
.typed_header(Accept::json())
.send(Context::default())
.await
.unwrap()
.try_into_json()
.await
.unwrap();
tracing::info!("info: {:?}", info);
assert_eq!(info.name, "Rama");
assert_eq!(info.example, "http_high_level_client.rs");
assert_eq!(info.magic, 42);
// Json Post + String Response Example
let resp = client
.post(format!("http://{ADDRESS}/introduce"))
.json(&json!({"name": "Rama"}))
.typed_header(Accept::text())
.send(Context::default())
.await
.unwrap()
.try_into_string()
.await
.unwrap();
tracing::info!("resp: {:?}", resp);
assert_eq!(resp, "Hello, Rama!");
// Example to show how to set basic auth directly while making request,
// this will now fail as the credentials are not authorized...
let resp = client
.get(format!("http://{ADDRESS}/info"))
.basic_auth("joe", "456")
.send(Context::default())
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
fn setup_tracing() {
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();
}
async fn run_server(addr: &str) {
// artificial delay to show the client retries
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
tracing::info!("running service at: {addr}");
let exec = Executor::default();
HttpServer::auto(exec)
.listen(
addr,
(
TraceLayer::new_for_http(),
CompressionLayer::new(),
AsyncRequireAuthorizationLayer::new(auth_request),
)
.into_layer(
WebService::default()
.get("/", "Hello, World!")
.get(
"/info",
async |req: Request| {
req.headers()
.get("x-magic")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map_or_else(
|| Json(json!({"name": "Rama", "example": "http_high_level_client.rs"})),
|magic| {
Json(json!({
"name": "Rama",
"example": "http_high_level_client.rs",
"magic": magic
}))
},
)
}
)
.post(
"/introduce",
async |Json(data): Json<serde_json::Value>| {
format!("Hello, {}!", data["name"].as_str().unwrap())
},
),
),
)
.await
.unwrap();
}
async fn auth_request<S>(ctx: Context<S>, req: Request) -> Result<(Context<S>, Request), Response> {
if req
.headers()
.typed_get::<Authorization<Basic>>()
.map(|auth| auth.username() == "john" && auth.password() == "123")
.unwrap_or_default()
{
tracing::info!("authorized request for {} from {}", req.uri(), "john");
Ok((ctx, req))
} else {
Err(StatusCode::UNAUTHORIZED.into_response())
}
}