Im trying to add a header to allow my API to be called from anywhere.
I try to use this (https://docs.rs/actix-web/latest/actix_web/http/header/constant.ACCESS_CONTROL_ALLOW_ORIGIN.html) with value * as the response header
I need that header for every request, so I think I need to wrap() it on App::new()
My cargo.toml:
actix-multipart = "0.4.0"
actix-web = "4.0.1"
actix-service = "2.0.2"
actix-rt = "2.2.0"
This code below doesn't work. Does anyone know how to do this ?
HttpServer::new(move || {
let api_service = web::scope("/api")
.configure(routes_provider)
.route("/", web::get().to(|| HttpResponse::Ok()));
App::new()
.wrap(Logger::default())
.wrap_fn(|req, srv| {
let fut = srv.call(req);
async {
let mut res = fut.await?;
res.headers_mut()
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
Ok(res)
}
})
.service(api_service)
})
.bind(bind_addr)?
.run()
.await
Error from client-side (React - Axios)
Have you tried to use the example from https://docs.rs/actix-cors/latest/actix_cors/ and with allow_any_origin?
Applied to your code it might look something like:
HttpServer::new(move || {
let api_service = web::scope("/api")
.configure(routes_provider)
.route("/", web::get().to(|| HttpResponse::Ok()));
let cors = Cors::default()
.allow_any_origin() // <--- this
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
App::new()
.wrap(cors)
.wrap(Logger::default())
.service(api_service)
})
.bind(bind_addr)?
.run()
.await
Related
I am trying to get the Vec<u8> or String (or more ideally a Blob ObjectURL) of a file uploaded as triggered by a button click.
I am guessing this will require an invisible <input> somewhere in the DOM but I can't figure out how to leverage web_sys and/or gloo to either get the contents nor a Blob ObjectURL.
A js-triggered input probably won't do the trick, as many browsers won't let you trigger a file input from JS, for good reasons. You can use labels to hid the input if you think it is ugly. Other than that, you need to wiggle yourself through the files api of HtmlInputElement. Pretty painful, that:
use js_sys::{Object, Reflect, Uint8Array};
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::*;
#[wasm_bindgen(start)]
pub fn init() {
// Just some setup for the example
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
let window = window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
while let Some(child) = body.first_child() {
body.remove_child(&child).unwrap();
}
// Create the actual input element
let input = document
.create_element("input")
.expect_throw("Create input")
.dyn_into::<HtmlInputElement>()
.unwrap();
input
.set_attribute("type", "file")
.expect_throw("Set input type file");
let recv_file = {
let input = input.clone();
Closure::<dyn FnMut()>::wrap(Box::new(move || {
let input = input.clone();
wasm_bindgen_futures::spawn_local(async move {
file_callback(input.files()).await;
})
}))
};
input
.add_event_listener_with_callback("change", recv_file.as_ref().dyn_ref().unwrap())
.expect_throw("Listen for file upload");
recv_file.forget(); // TODO: this leaks. I forgot how to get around that.
body.append_child(&input).unwrap();
}
async fn file_callback(files: Option<FileList>) {
let files = match files {
Some(files) => files,
None => return,
};
for i in 0..files.length() {
let file = match files.item(i) {
Some(file) => file,
None => continue,
};
console::log_2(&"File:".into(), &file.name().into());
let reader = file
.stream()
.get_reader()
.dyn_into::<ReadableStreamDefaultReader>()
.expect_throw("Reader is reader");
let mut data = Vec::new();
loop {
let chunk = JsFuture::from(reader.read())
.await
.expect_throw("Read")
.dyn_into::<Object>()
.unwrap();
// ReadableStreamReadResult is somehow wrong. So go by hand. Might be a web-sys bug.
let done = Reflect::get(&chunk, &"done".into()).expect_throw("Get done");
if done.is_truthy() {
break;
}
let chunk = Reflect::get(&chunk, &"value".into())
.expect_throw("Get chunk")
.dyn_into::<Uint8Array>()
.expect_throw("bytes are bytes");
let data_len = data.len();
data.resize(data_len + chunk.length() as usize, 255);
chunk.copy_to(&mut data[data_len..]);
}
console::log_2(
&"Got data".into(),
&String::from_utf8_lossy(&data).into_owned().into(),
);
}
}
(If you've got questions about the code, ask. But it's too much to explain it in detail.)
And extra, the features you need on web-sys for this to work:
[dependencies.web-sys]
version = "0.3.60"
features = ["Window", "Navigator", "console", "Document", "HtmlInputElement", "Event", "EventTarget", "FileList", "File", "Blob", "ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamReadResult"]
Thanks to Caesar I ended up with this code for use with dominator as the Dom crate.
pub fn upload_file_input(mimes: &str, mutable: Mutable<Vec<u8>>) -> Dom {
input(|i| {
i.class("file-input")
.prop("type", "file")
.prop("accept", mimes)
.apply(|el| {
let element: HtmlInputElement = el.__internal_element();
let recv_file = {
let input = element.clone();
Closure::<dyn FnMut()>::wrap(Box::new(move || {
let input = input.clone();
let mutable = mutable.clone();
wasm_bindgen_futures::spawn_local(async move {
file_callback(input.files(), mutable.clone()).await;
})
}))
};
element
.add_event_listener_with_callback(
"change",
recv_file.as_ref().dyn_ref().unwrap(),
)
.expect("Listen for file upload");
recv_file.forget();
el
})
})
}
async fn file_callback(files: Option<FileList>, mutable: Mutable<Vec<u8>>) {
let files = match files {
Some(files) => files,
None => return,
};
for i in 0..files.length() {
let file = match files.item(i) {
Some(file) => file,
None => continue,
};
// gloo::console::console_dbg!("File:", &file.name());
let reader = file
.stream()
.get_reader()
.dyn_into::<ReadableStreamDefaultReader>()
.expect("Reader is reader");
let mut data = Vec::new();
loop {
let chunk = JsFuture::from(reader.read())
.await
.expect("Read")
.dyn_into::<Object>()
.unwrap();
// ReadableStreamReadResult is somehow wrong. So go by hand. Might be a web-sys bug.
let done = Reflect::get(&chunk, &"done".into()).expect("Get done");
if done.is_truthy() {
break;
}
let chunk = Reflect::get(&chunk, &"value".into())
.expect("Get chunk")
.dyn_into::<Uint8Array>()
.expect("bytes are bytes");
let data_len = data.len();
data.resize(data_len + chunk.length() as usize, 255);
chunk.copy_to(&mut data[data_len..]);
}
mutable.set(data);
// gloo::console::console_dbg!(
// "Got data",
// &String::from_utf8_lossy(&data).into_owned(),
// );
}
}
I am using actix-web.
I would like to restrict the size of post request and have the web application capture this error log it and send back an informative error message to the user.
My code is given below.
It seems the middleware gets run whether or not the limit is exceeded and HTTP code 413 raised.
The accept_post is executed only when the limit has not been exceeded.
Is there a way of capturing this error in the middleware?
.service(
web::scope("/hello_someone4")
.service(
web::resource("/there/*")
.app_data(web::PayloadConfig::default().limit(16))
.wrap(simple_middleware::simple_middleware_say_hi::SayHi)
.route(web::post().to(request_handlers2::accept_post))
)
)
The actix_web::middleware::errhandlers middleware provides the solutions I was seeking.
The example code below as obtained from "https://actix.rs/docs/middleware/".
I can now add code to the render_413 function (shown below) to perform the logging.
use actix_web::middleware::errhandlers::{ErrorHandlerResponse, ErrorHandlers};
use actix_web::{dev, http, HttpResponse, Result};
fn render_500<B>(mut res: actix_web::dev::ServiceResponse<B>) -> actix_web::Result<actix_web::middleware::errhandlers::ErrorHandlerResponse<B>> {
res.response_mut()
.headers_mut()
.insert(actix_web::http::header::CONTENT_TYPE, actix_web::http::HeaderValue::from_static("Error"));
Ok(actix_web::middleware::errhandlers::ErrorHandlerResponse::Response(res))
}
fn render_413<B>(mut res: actix_web::dev::ServiceResponse<B>) -> actix_web::Result<actix_web::middleware::errhandlers::ErrorHandlerResponse<B>> {
res.response_mut()
.headers_mut()
.insert(actix_web::http::header::CONTENT_TYPE, actix_web::http::HeaderValue::from_static("413 Error"));
let req = res.request();
let res = res.map_body(|_, _| actix_web::body::ResponseBody::Body(actix_web::dev::Body::from("{\"code\":413,\"error\":\"413 Payload Too Large\",\"message\":\"You've sent more data than expected\"}")).into_body());//alter the the response body see "https://users.rust-lang.org/t/actix-web-using-a-custom-error-handler-to-alter-the-response-body/41068"
Ok(actix_web::middleware::errhandlers::ErrorHandlerResponse::Response(res))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use actix_web::{web, App, HttpServer};
HttpServer::new(|| {
App::new()
.wrap(
actix_web::middleware::errhandlers::ErrorHandlers::new()
.handler(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, render_500),
)
.wrap(
actix_web::middleware::errhandlers::ErrorHandlers::new()
.handler(actix_web::http::StatusCode::PAYLOAD_TOO_LARGE, render_413),
)
)
.service(
web::resource("/test")
.route(web::get().to(|| HttpResponse::Ok()))
.route(web::head().to(|| HttpResponse::MethodNotAllowed())),
)
.service(
web::scope("/hello_someone4")
.service(
web::resource("/there/*")
.app_data(web::PayloadConfig::default().limit(16))
.wrap(simple_middleware::simple_middleware_say_hi::SayHi)
.route(web::post().to(request_handlers2::accept_post))
)
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
I'm creating my own frontend client to upload videos to a video hosting service.
According to the API docs, I am to POST a multipart/form:
curl -X POST \
-H "Key: YOUR_KEY" \
-F "file=#hello_world.mp4" \
https://muse.ai/api/files/upload
I could setup my client to be allowed to make that cross origin request, however, the request requires a secret API key, so I need my server to proxy the request on behalf of my client.
As minimally follows:
use actix_web::{Error, HttpResponse, client::Client, error, web, HttpRequest};
use std::env;
pub async fn upload_video(req: HttpRequest, payload: web::Payload) -> Result<HttpResponse, Error> {
let muse_api_key = match env::var("MUSE_AI") {
Ok(token) => token,
Err(e) => {
return Err(error::ErrorInternalServerError(e));
}
};
let client = Client::default();
let mut forward_req_resp = client
.request_from("https://muse.ai/api/files/upload", req.head())
.header("key", muse_api_key)
.send_stream(payload)
.await?;
let mut client_resp = HttpResponse::build(forward_req_resp.status());
Ok(client_resp.body(forward_req_resp.body().await?))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
App::new()
.service(
web::scope("/video")
.service(
web::resource("/upload").route(web::post().to(upload_video)),
)
)
})
.bind("127.0.0.1:8001")?
.run()
.await
}
Prior to wiring up my frontend, I'm testing with cURL:
curl -v -X POST -F "file=#/path/to/sintel_trailer-720p.mp4" 127.0.0.1:8001/video/upload
Resulting output:
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> POST /video/upload HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 7608419
> Content-Type: multipart/form-data; boundary=------------------------076c45bb618a62c2
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
< HTTP/1.1 400 Bad Request
< content-length: 40
< content-type: text/plain; charset=utf-8
< set-cookie: actix-session=3JVldgobuv9nv6zE9uV%2F1i0slQMHlV384rXzc9BWwRE%3D%7B%7D; HttpOnly; Path=/
< date: Mon, 01 Feb 2021 07:32:14 GMT
* HTTP error before end of send, stop sending
<
* Closing connection 0
protocol error: not a result of an error%
I've tried of a few variants, maybe one of which is notable:
Adding the following to top of handler function:
let mut bytes = web::BytesMut::new();
while let Some(item) = payload.next().await {
bytes.extend_from_slice(&item?);
}
Then changing ClientRequestBuilding from .request_from to:
.post("https://muse.ai/api/files/upload")
This results in: { error: "missing_file"}.
I figured out a solution that technically works but it's not really what I originally intended, as I would prefer to forward the multipart/form-data stream along, instead of writing the file part of the stream to a local file, which could easily take up available memory at scale.
Although I'm answering my own question, I'd love additional input to fill in my missing gaps.
Solution:
use reqwest::blocking;
use actix_multipart::Multipart;
pub async fn upload_video(mut payload: Multipart) -> Result<HttpResponse, Error> {
let muse_api_key = match env::var("MUSE_AI") {
Ok(token) => token,
Err(e) => {
return Err(error::ErrorInternalServerError(e));
},
};
let mut filepath_copy = String::from("");
while let Ok(Some(mut field)) = payload.try_next().await {
let content_type = field.content_disposition().unwrap();
let filename = content_type.get_filename().unwrap();
let filepath = format!("./{}", sanitize_filename::sanitize(&filename));
filepath_copy = format!("./{}", sanitize_filename::sanitize(&filename));
let mut f = web::block(|| std::fs::File::create(filepath))
.await?;
while let Some(chunk) = field.next().await {
let data = chunk.unwrap();
f = web::block(move || f.write_all(&data).map(|_| f)).await?;
}
}
let client = blocking::Client::new();
let form = blocking::multipart::Form::new()
.file("file", filepath_copy.clone())?;
let forward_req_resp = web::block(move ||
client
.post("https://muse.ai/api/files/upload")
.header("key", muse_api_key)
.multipart(form)
.send()
).await?;
web::block(|| fs::remove_file(filepath_copy)).await?;
let status = forward_req_resp.status();
Ok(HttpResponse::build(status).finish())
}
Dependences (used for this handler, not the whole app):
futures = "0.3.12"
sanitize-filename = "0.3.0"
reqwest = { version = "0.11.0", features = ["multipart", "blocking", "json"] }
actix-multipart = "0.3.0"
Currently my main function, where the server starts looks like this
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let address = "0.0.0.0:3000";
HttpServer::new(move || {
let app_state = {...some state};
App::new()
.data(app_state)
.wrap(middleware::Logger::default())
.service(network_api::init_service())
})
.bind(address)?
.run()
.await
}
Once the server starts, I want to run one (async) function,
to make a request to another server, and that server should respond with another request to this server
that just got up and running.
Not sure I see in the documentation anything mentioning a callback function that is run only once on server start.
For example maybe having it in run() function, like this:
run(|| {
// server started now perform some request or whatever
Client::new().post(&url).json(json_stuff).send().await
})
Edit
I think I solved it, will post answer when I can answer my own question
I have solved this by joining 2 async functions,
created another async function
async fn another_func() -> Result<()> {
...
}
and have used future::join() like this
let server = HttpServer::new(move || {
let app_state = {...some state};
App::new()
.data(app_state)
.wrap(middleware::Logger::default())
.service(network_api::init_service())
})
.bind(address)?
.run();
future::join(server, another_func()).await;
Ok(()
Of course if anyone has a better answer please post it
I actually struggled with it a bit and eventually replaced the future::join with tokio join! macro as follows:
tokio::join!(server, utils::open_web_app(server_address));
Ok(())
I have an actix-web server using HttpAuthentication middleware to authenticate all requests. The server runs fine and responds correctly to most requests, but occasionally certain requests trigger the error:
thread 'actix-rt:worker:2' panicked at 'AuthenticationMiddleware was called already
A request to the same endpoint will only trigger the error some of the time, so I am not sure what the root cause is.
My main() function (with only the relevant code included is:
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
// Some configuration code here
HttpServer::new(move || {
App::new()
.wrap(
HttpAuthentication::basic(validator)
)
// other code here
})
.bind(ip)?
.run()
.await
}
The validator function passed as the process_fn argument to HttpAuthentication::basic is:
async fn validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
let config = req.app_data::<Config>()
.map(|data| data.get_ref().clone())
.unwrap_or_else(Default::default)
.scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
let username = env::var("USERNAME")
.expect("USERNAME must be set");
let password = env::var("PASSWORD")
.expect("USERNAME must be set");
if credentials.user_id().deref() == username {
match credentials.password() {
Some(pass) => {
if pass.deref() == password {
Ok(req)
} else {
Err(AuthenticationError::from(config).into())
}
}
None => Err(AuthenticationError::from(config).into())
}
} else {
Err(AuthenticationError::from(config).into())
}
}
This function essentially is just checking for the validity of the basic authentication username and password sent in the request. As I understand it, this should be wrapping every endpoint on the server and only allowing authenticated requests through.
What I do not understand is why I am getting this runtime error. Does anyone have any ideas as to why this is happening?