Skip to content

Add backend proxy to aid local fastboot testing #2215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 72 additions & 16 deletions src/middleware/ember_index_rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@
//! with "/api" and the Accept header contains "html".

use super::prelude::*;
use std::fmt::Write;

use crate::util::RequestProxy;
use crate::util::{errors::NotFound, AppResponse, Error, RequestProxy};

use conduit::{Body, HandlerResult};
use reqwest::blocking::Client;

// Can't derive debug because of Handler and Static.
#[allow(missing_debug_implementations)]
pub struct EmberIndexRewrite {
handler: Option<Box<dyn Handler>>,
fastboot_client: Option<Client>,
}

impl Default for EmberIndexRewrite {
fn default() -> EmberIndexRewrite {
EmberIndexRewrite { handler: None }
let fastboot_client = match dotenv::var("USE_FASTBOOT") {
Ok(val) if val == "staging-experimental" => Some(Client::new()),
_ => None,
};

EmberIndexRewrite {
handler: None,
fastboot_client,
}
}
}

Expand All @@ -24,22 +37,65 @@ impl AroundMiddleware for EmberIndexRewrite {
}

impl Handler for EmberIndexRewrite {
fn call(&self, req: &mut dyn RequestExt) -> AfterResult {
// If the client is requesting html, then we've only got one page so
// rewrite the request.
let wants_html = req
.headers()
.get_all(header::ACCEPT)
.iter()
.any(|val| val.to_str().unwrap_or_default().contains("html"));
// If the route starts with /api, just assume they want the API
// response and fall through.
let is_api_path = req.path().starts_with("/api");
fn call(&self, req: &mut dyn RequestExt) -> HandlerResult {
let handler = self.handler.as_ref().unwrap();
if wants_html && !is_api_path {
handler.call(&mut RequestProxy::rewrite_path(req, "/index.html"))
} else {

if req.path().starts_with("/api") {
handler.call(req)
} else {
if let Some(client) = &self.fastboot_client {
// During local fastboot development, forward requests to the local fastboot server.
// In prodution, including when running with fastboot, nginx proxies the requests
// to the correct endpoint and requests should never make it here.
return proxy_to_fastboot(client, req).map_err(box_error);
}

if req
.headers()
.get_all(header::ACCEPT)
.iter()
.any(|val| val.to_str().unwrap_or_default().contains("html"))
{
// Serve static Ember page to bootstrap the frontend
handler.call(&mut RequestProxy::rewrite_path(req, "/index.html"))
} else {
// Return a 404 to crawlers that don't send `Accept: text/hml`.
// This is to preserve legacy behavior and will likely change.
// Most of these crawlers probably won't execute our frontend JS anyway, but
// it would be nice to bootstrap the app for crawlers that do execute JS.
Ok(NotFound.into())
}
}
}
}

/// Proxy to the fastboot server in development mode
///
/// This handler is somewhat hacky, and is not intended for usage in production.
///
/// # Panics
///
/// This function can panic and should only be used in development mode.
fn proxy_to_fastboot(client: &Client, req: &mut dyn RequestExt) -> Result<AppResponse, Error> {
if req.method() != conduit::Method::GET {
return Err(format!("Only support GET but request method was {}", req.method()).into());
}

let mut url = format!("http://127.0.0.1:9000{}", req.path());
if let Some(query) = req.query_string() {
write!(url, "?{}", query).map_err(|e| e.to_string())?;
}
let mut fastboot_response = client
.request(req.method().into(), &*url)
.headers(req.headers().clone())
.send()?;
let mut body = Vec::new();
fastboot_response.copy_to(&mut body)?;

let mut builder = Response::builder().status(fastboot_response.status());
builder
.headers_mut()
.unwrap()
.extend(fastboot_response.headers().clone());
builder.body(Body::from_vec(body)).map_err(Into::into)
}
8 changes: 7 additions & 1 deletion src/util/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,15 @@ impl AppError for InternalAppError {
#[derive(Debug, Clone, Copy)]
pub struct NotFound;

impl From<NotFound> for AppResponse {
fn from(_: NotFound) -> AppResponse {
json_error("Not Found", StatusCode::NOT_FOUND)
}
}

impl AppError for NotFound {
fn response(&self) -> Option<AppResponse> {
Some(json_error("Not Found", StatusCode::NOT_FOUND))
Some(NotFound.into())
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/util/errors/concrete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub enum Error {
DbConnect(diesel::result::ConnectionError),
DbQuery(diesel::result::Error),
DotEnv(dotenv::Error),
Http(http::Error),
Internal(String),
Io(io::Error),
JobEnqueue(swirl::EnqueueError),
Expand All @@ -20,6 +21,7 @@ impl fmt::Display for Error {
Error::DbConnect(inner) => inner.fmt(f),
Error::DbQuery(inner) => inner.fmt(f),
Error::DotEnv(inner) => inner.fmt(f),
Error::Http(inner) => inner.fmt(f),
Error::Internal(inner) => inner.fmt(f),
Error::Io(inner) => inner.fmt(f),
Error::JobEnqueue(inner) => inner.fmt(f),
Expand Down Expand Up @@ -47,6 +49,12 @@ impl From<dotenv::Error> for Error {
}
}

impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
Error::Http(err)
}
}

impl From<String> for Error {
fn from(err: String) -> Self {
Error::Internal(err)
Expand Down