diff --git a/src/middleware/ember_index_rewrite.rs b/src/middleware/ember_index_rewrite.rs index fbcc011a8d1..b42b7cab665 100644 --- a/src/middleware/ember_index_rewrite.rs +++ b/src/middleware/ember_index_rewrite.rs @@ -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>, + fastboot_client: Option, } 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, + } } } @@ -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 { + 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) +} diff --git a/src/util/errors.rs b/src/util/errors.rs index 727cbb99409..c2e087fa12e 100644 --- a/src/util/errors.rs +++ b/src/util/errors.rs @@ -226,9 +226,15 @@ impl AppError for InternalAppError { #[derive(Debug, Clone, Copy)] pub struct NotFound; +impl From for AppResponse { + fn from(_: NotFound) -> AppResponse { + json_error("Not Found", StatusCode::NOT_FOUND) + } +} + impl AppError for NotFound { fn response(&self) -> Option { - Some(json_error("Not Found", StatusCode::NOT_FOUND)) + Some(NotFound.into()) } } diff --git a/src/util/errors/concrete.rs b/src/util/errors/concrete.rs index 54491f00a39..54c2d9d0b3a 100644 --- a/src/util/errors/concrete.rs +++ b/src/util/errors/concrete.rs @@ -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), @@ -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), @@ -47,6 +49,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: http::Error) -> Self { + Error::Http(err) + } +} + impl From for Error { fn from(err: String) -> Self { Error::Internal(err)