mirror of
https://github.com/actix/examples
synced 2025-06-26 17:17:42 +02:00
Restructure folders (#411)
This commit is contained in:
committed by
GitHub
parent
9db98162b2
commit
c3407627d0
9
json/json_decode_error/Cargo.toml
Normal file
9
json/json_decode_error/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "json_decode_error"
|
||||
version = "0.1.0"
|
||||
authors = ["Stig Johan Berggren <stigjb@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
serde = "1"
|
75
json/json_decode_error/README.md
Normal file
75
json/json_decode_error/README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# JSON decode errors
|
||||
|
||||
This example demonstrates how to return useful error messages to the client
|
||||
when the server receives a request with invalid JSON, or which cannot be
|
||||
deserialized to the expected model. By configuring an `error_handler` on the
|
||||
route, we can set appropriate response codes and return the string
|
||||
representation of the error.
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
cd examples/json_decode_error
|
||||
cargo run
|
||||
# Started HTTP server: 127.0.0.1:8088
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The examples use `curl -i` in order to show the status line with the response
|
||||
code. The response headers have been omitted for brevity, and replaced with an
|
||||
ellipsis `...`.
|
||||
|
||||
- A well-formed request
|
||||
|
||||
```shell
|
||||
$ curl -i 127.0.0.1:8088 -H 'Content-Type: application/json' -d '{"name": "Alice"}'
|
||||
HTTP/1.1 200 OK
|
||||
...
|
||||
|
||||
Hello Alice!
|
||||
```
|
||||
|
||||
- Missing `Content-Type` header
|
||||
|
||||
```shell
|
||||
$ curl -i 127.0.0.1:8088 -d '{"name": "Bob"}'
|
||||
HTTP/1.1 415 Unsupported Media Type
|
||||
...
|
||||
|
||||
Content type error
|
||||
```
|
||||
|
||||
- Malformed JSON
|
||||
|
||||
```shell
|
||||
$ curl -i 127.0.0.1:8088 -H 'Content-Type: application/json' -d '{"name": "Eve}'
|
||||
HTTP/1.1 400 Bad Request
|
||||
...
|
||||
|
||||
Json deserialize error: EOF while parsing a string at line 1 column 14
|
||||
```
|
||||
|
||||
- JSON value of wrong type
|
||||
|
||||
```shell
|
||||
$ curl -i 127.0.0.1:8088 -H 'Content-Type: application/json' -d '{"name": 350}'
|
||||
HTTP/1.1 422 Unprocessable Entity
|
||||
...
|
||||
|
||||
Json deserialize error: invalid type: integer `350`, expected a string at line 1 column 12
|
||||
```
|
||||
|
||||
- Wrong JSON key
|
||||
|
||||
```shell
|
||||
$ curl -i 127.0.0.1:8088 -H 'Content-Type: application/json' -d '{"namn": "John"}'
|
||||
HTTP/1.1 422 Unprocessable Entity
|
||||
...
|
||||
|
||||
Json deserialize error: missing field `name` at line 1 column 16
|
||||
```
|
||||
|
||||
## More documentation
|
||||
|
||||
[`actix_web::web::JsonConfig`](https://docs.rs/actix-web/latest/actix_web/web/struct.JsonConfig.html)
|
44
json/json_decode_error/src/main.rs
Normal file
44
json/json_decode_error/src/main.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use actix_web::{
|
||||
error, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Info {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[post("/")]
|
||||
async fn greet(name: web::Json<Info>) -> impl Responder {
|
||||
HttpResponse::Ok().body(format!("Hello {}!", name.name))
|
||||
}
|
||||
|
||||
fn json_error_handler(err: error::JsonPayloadError, _req: &HttpRequest) -> error::Error {
|
||||
use actix_web::error::JsonPayloadError;
|
||||
|
||||
let detail = err.to_string();
|
||||
let resp = match &err {
|
||||
JsonPayloadError::ContentType => {
|
||||
HttpResponse::UnsupportedMediaType().body(detail)
|
||||
}
|
||||
JsonPayloadError::Deserialize(json_err) if json_err.is_data() => {
|
||||
HttpResponse::UnprocessableEntity().body(detail)
|
||||
}
|
||||
_ => HttpResponse::BadRequest().body(detail),
|
||||
};
|
||||
error::InternalError::from_response(err, resp).into()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| {
|
||||
App::new().service(greet).app_data(
|
||||
web::JsonConfig::default()
|
||||
// register error_handler for JSON extractors.
|
||||
.error_handler(json_error_handler),
|
||||
)
|
||||
})
|
||||
.bind("127.0.0.1:8088")?
|
||||
.run()
|
||||
.await
|
||||
}
|
11
json/json_error/Cargo.toml
Normal file
11
json/json_error/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "json_error"
|
||||
version = "1.0.0"
|
||||
authors = ["Kai Yao <kai.b.yao@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
failure = "0.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
50
json/json_error/src/main.rs
Normal file
50
json/json_error/src/main.rs
Normal file
@ -0,0 +1,50 @@
|
||||
// This example is meant to show how to automatically generate a json error response when something goes wrong.
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
use std::io;
|
||||
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, App, HttpServer, ResponseError};
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, to_string_pretty};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Error {
|
||||
msg: String,
|
||||
status: u16,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
write!(f, "{}", to_string_pretty(self).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for Error {
|
||||
// builds the actual response to send back when an error occurs
|
||||
fn error_response(&self) -> web::HttpResponse {
|
||||
let err_json = json!({ "error": self.msg });
|
||||
web::HttpResponse::build(StatusCode::from_u16(self.status).unwrap())
|
||||
.json(err_json)
|
||||
}
|
||||
}
|
||||
|
||||
async fn index() -> Result<web::HttpResponse, Error> {
|
||||
Err(Error {
|
||||
msg: "an example error message".to_string(),
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
let ip_address = "127.0.0.1:8000";
|
||||
println!("Running server on {}", ip_address);
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new().service(web::resource("/").route(web::get().to(index)))
|
||||
})
|
||||
.bind(ip_address)
|
||||
.expect("Can not bind to port 8000")
|
||||
.run()
|
||||
.await
|
||||
}
|
14
json/jsonrpc/Cargo.toml
Normal file
14
json/jsonrpc/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "jsonrpc-example"
|
||||
version = "2.0.0"
|
||||
authors = ["mohanson <mohanson@outlook.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
bytes = "0.5"
|
||||
env_logger = "0.8"
|
||||
futures = "0.3.1"
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
34
json/jsonrpc/README.md
Normal file
34
json/jsonrpc/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
A simple demo for building a `JSONRPC over HTTP` server in [actix-web](https://github.com/actix/actix-web).
|
||||
|
||||
# Server
|
||||
|
||||
```sh
|
||||
$ cargo run
|
||||
# Starting server on 127.0.0.1:8080
|
||||
```
|
||||
|
||||
# Client
|
||||
|
||||
**curl**
|
||||
|
||||
```sh
|
||||
$ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ping", "params": [], "id": 1}' http://127.0.0.1:8080
|
||||
# {"jsonrpc":"2.0","result":"pong","error":null,"id":1}
|
||||
```
|
||||
|
||||
|
||||
**python**
|
||||
|
||||
```sh
|
||||
$ python tests\test_client.py
|
||||
# {'jsonrpc': '2.0', 'result': 'pong', 'error': None, 'id': 1}
|
||||
```
|
||||
|
||||
# Methods
|
||||
|
||||
- `ping`: Pong immeditely
|
||||
- `wait`: Wait `n` seconds, and then pong
|
||||
- `get`: Get global count
|
||||
- `inc`: Increment global count
|
||||
|
||||
See `tests\test_client.py` to get more information.
|
137
json/jsonrpc/src/convention.rs
Normal file
137
json/jsonrpc/src/convention.rs
Normal file
@ -0,0 +1,137 @@
|
||||
//! JSON-RPC 2.0 Specification
|
||||
//! See: https://www.jsonrpc.org/specification
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
pub static JSONRPC_VERSION: &str = "2.0";
|
||||
|
||||
/// When a rpc call encounters an error, the Response Object MUST contain the
|
||||
/// error member with a value that is a Object with the following members:
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ErrorData {
|
||||
/// A Number that indicates the error type that occurred. This MUST be an integer.
|
||||
pub code: i32,
|
||||
|
||||
/// A String providing a short description of the error. The message SHOULD be
|
||||
/// limited to a concise single sentence.
|
||||
pub message: String,
|
||||
|
||||
/// A Primitive or Structured value that contains additional information
|
||||
/// about the error. This may be omitted. The value of this member is
|
||||
/// defined by the Server (e.g. detailed error information, nested errors
|
||||
/// etc.).
|
||||
pub data: Value,
|
||||
}
|
||||
|
||||
impl ErrorData {
|
||||
pub fn new(code: i32, message: &str) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: String::from(message),
|
||||
data: Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn std(code: i32) -> Self {
|
||||
match code {
|
||||
// Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
|
||||
-32700 => ErrorData::new(-32700, "Parse error"),
|
||||
// The JSON sent is not a valid Request object.
|
||||
-32600 => ErrorData::new(-32600, "Invalid Request"),
|
||||
// The method does not exist / is not available.
|
||||
-32601 => ErrorData::new(-32601, "Method not found"),
|
||||
// Invalid method parameter(s).
|
||||
-32602 => ErrorData::new(-32602, "Invalid params"),
|
||||
// Internal JSON-RPC error.
|
||||
-32603 => ErrorData::new(-32603, "Internal error"),
|
||||
// The error codes from and including -32768 to -32000 are reserved for pre-defined errors. Any code within
|
||||
// this range, but not defined explicitly below is reserved for future use.
|
||||
_ => panic!("Undefined pre-defined error codes"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints out the value as JSON string.
|
||||
pub fn dump(&self) -> String {
|
||||
serde_json::to_string(self).expect("Should never failed")
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for ErrorData {}
|
||||
impl fmt::Display for ErrorData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({}, {}, {})", self.code, self.message, self.data)
|
||||
}
|
||||
}
|
||||
|
||||
/// A rpc call is represented by sending a Request object to a Server.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
/// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
|
||||
pub jsonrpc: String,
|
||||
|
||||
/// A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by
|
||||
/// a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be
|
||||
/// used for anything else.
|
||||
pub method: String,
|
||||
|
||||
/// A Structured value that holds the parameter values to be used during the invocation of the method. This member
|
||||
/// MAY be omitted.
|
||||
pub params: Vec<Value>,
|
||||
|
||||
/// An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is
|
||||
/// not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD
|
||||
/// NOT contain fractional parts.
|
||||
pub id: Value,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Prints out the value as JSON string.
|
||||
pub fn dump(&self) -> String {
|
||||
serde_json::to_string(self).expect("Should never failed")
|
||||
}
|
||||
}
|
||||
|
||||
/// When a rpc call is made, the Server MUST reply with a Response, except for in the case of Notifications. The
|
||||
/// Response is expressed as a single JSON Object, with the following members:
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Response {
|
||||
/// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
|
||||
pub jsonrpc: String,
|
||||
|
||||
/// This member is REQUIRED on success.
|
||||
/// This member MUST NOT exist if there was an error invoking the method.
|
||||
/// The value of this member is determined by the method invoked on the Server.
|
||||
pub result: Value,
|
||||
|
||||
// This member is REQUIRED on error.
|
||||
// This member MUST NOT exist if there was no error triggered during invocation.
|
||||
// The value for this member MUST be an Object as defined in section 5.1.
|
||||
pub error: Option<ErrorData>,
|
||||
|
||||
/// This member is REQUIRED.
|
||||
/// It MUST be the same as the value of the id member in the Request Object.
|
||||
/// If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request),
|
||||
/// it MUST be Null.
|
||||
pub id: Value,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Prints out the value as JSON string.
|
||||
pub fn dump(&self) -> String {
|
||||
serde_json::to_string(self).expect("Should never failed")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Response {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
jsonrpc: JSONRPC_VERSION.into(),
|
||||
result: Value::Null,
|
||||
error: None,
|
||||
id: Value::Null,
|
||||
}
|
||||
}
|
||||
}
|
161
json/jsonrpc/src/main.rs
Normal file
161
json/jsonrpc/src/main.rs
Normal file
@ -0,0 +1,161 @@
|
||||
// Allow this lint since it's fine to use type directly in the short example.
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use std::error;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer};
|
||||
use bytes::Bytes;
|
||||
use futures::{Future, FutureExt};
|
||||
use serde_json::Value;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod convention;
|
||||
|
||||
/// The main handler for JSONRPC server.
|
||||
async fn rpc_handler(
|
||||
body: Bytes,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let reqjson: convention::Request = match serde_json::from_slice(body.as_ref()) {
|
||||
Ok(ok) => ok,
|
||||
Err(_) => {
|
||||
let r = convention::Response {
|
||||
jsonrpc: String::from(convention::JSONRPC_VERSION),
|
||||
result: Value::Null,
|
||||
error: Some(convention::ErrorData::std(-32700)),
|
||||
id: Value::Null,
|
||||
};
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(r.dump()));
|
||||
}
|
||||
};
|
||||
let mut result = convention::Response::default();
|
||||
result.id = reqjson.id.clone();
|
||||
|
||||
match rpc_select(&app_state, reqjson.method.as_str(), reqjson.params).await {
|
||||
Ok(ok) => result.result = ok,
|
||||
Err(e) => result.error = Some(e),
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(result.dump()))
|
||||
}
|
||||
|
||||
async fn rpc_select(
|
||||
app_state: &AppState,
|
||||
method: &str,
|
||||
params: Vec<Value>,
|
||||
) -> Result<Value, convention::ErrorData> {
|
||||
match method {
|
||||
"ping" => {
|
||||
let r = app_state.network.read().unwrap().ping();
|
||||
Ok(Value::from(r))
|
||||
}
|
||||
"wait" => {
|
||||
if params.len() != 1 || !params[0].is_u64() {
|
||||
return Err(convention::ErrorData::std(-32602));
|
||||
}
|
||||
match app_state
|
||||
.network
|
||||
.read()
|
||||
.unwrap()
|
||||
.wait(params[0].as_u64().unwrap())
|
||||
.await
|
||||
{
|
||||
Ok(ok) => Ok(Value::from(ok)),
|
||||
Err(e) => Err(convention::ErrorData::new(500, &format!("{:?}", e)[..])),
|
||||
}
|
||||
}
|
||||
"get" => {
|
||||
let r = app_state.network.read().unwrap().get();
|
||||
Ok(Value::from(r))
|
||||
}
|
||||
"inc" => {
|
||||
app_state.network.write().unwrap().inc();
|
||||
Ok(Value::Null)
|
||||
}
|
||||
_ => Err(convention::ErrorData::std(-32601)),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ImplNetwork {
|
||||
fn ping(&self) -> String;
|
||||
fn wait(
|
||||
&self,
|
||||
d: u64,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String, Box<dyn error::Error>>>>>;
|
||||
|
||||
fn get(&self) -> u32;
|
||||
fn inc(&mut self);
|
||||
}
|
||||
|
||||
pub struct ObjNetwork {
|
||||
c: u32,
|
||||
}
|
||||
|
||||
impl ObjNetwork {
|
||||
fn new() -> Self {
|
||||
Self { c: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplNetwork for ObjNetwork {
|
||||
fn ping(&self) -> String {
|
||||
String::from("pong")
|
||||
}
|
||||
|
||||
fn wait(
|
||||
&self,
|
||||
d: u64,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String, Box<dyn error::Error>>>>> {
|
||||
async move {
|
||||
actix_web::rt::time::delay_for(Duration::from_secs(d)).await;
|
||||
Ok(String::from("pong"))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn get(&self) -> u32 {
|
||||
self.c
|
||||
}
|
||||
|
||||
fn inc(&mut self) {
|
||||
self.c += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
network: Arc<RwLock<dyn ImplNetwork>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(network: Arc<RwLock<dyn ImplNetwork>>) -> Self {
|
||||
Self { network }
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
env_logger::init();
|
||||
|
||||
let network = Arc::new(RwLock::new(ObjNetwork::new()));
|
||||
|
||||
HttpServer::new(move || {
|
||||
let app_state = AppState::new(network.clone());
|
||||
App::new()
|
||||
.data(app_state)
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(web::resource("/").route(web::post().to(rpc_handler)))
|
||||
})
|
||||
.bind("127.0.0.1:8080")
|
||||
.unwrap()
|
||||
.run()
|
||||
.await
|
||||
}
|
38
json/jsonrpc/tests/test_client.py
Normal file
38
json/jsonrpc/tests/test_client.py
Normal file
@ -0,0 +1,38 @@
|
||||
import requests
|
||||
|
||||
print('ping: pong immediately')
|
||||
r = requests.post('http://127.0.0.1:8080/', json={
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'ping',
|
||||
'params': [],
|
||||
'id': 1
|
||||
})
|
||||
print(r.json())
|
||||
|
||||
|
||||
print('ping: pong after 4 secs')
|
||||
r = requests.post('http://127.0.0.1:8080/', json={
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'wait',
|
||||
'params': [4],
|
||||
'id': 1
|
||||
})
|
||||
print(r.json())
|
||||
|
||||
for i in range(10):
|
||||
print(f'inc {i:>02}')
|
||||
r = requests.post('http://127.0.0.1:8080/', json={
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'inc',
|
||||
'params': [],
|
||||
'id': 1
|
||||
})
|
||||
|
||||
print(f'get')
|
||||
r = requests.post('http://127.0.0.1:8080/', json={
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'get',
|
||||
'params': [],
|
||||
'id': 1
|
||||
})
|
||||
print(r.json())
|
Reference in New Issue
Block a user