Skip to content

Commit 2ba19be

Browse files
committed
Auto merge of #2215 - jtgeibel:backend-fastboot-proxy, r=JohnTitor
Add backend proxy to aid local fastboot testing To avoid the need to set up a local nginx configuration during development, this commit adds a simple proxy for frontend requests when `USE_FASTBOOT=staging-experimental` is set. When enabled, developers should browse to http://localhost:8888/ (instead of port 4200 when using `npm run start:local`).
2 parents 1fff9e9 + c9b9ef0 commit 2ba19be

File tree

3 files changed

+87
-17
lines changed

3 files changed

+87
-17
lines changed

src/middleware/ember_index_rewrite.rs

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,31 @@
22
//! with "/api" and the Accept header contains "html".
33
44
use super::prelude::*;
5+
use std::fmt::Write;
56

6-
use crate::util::RequestProxy;
7+
use crate::util::{errors::NotFound, AppResponse, Error, RequestProxy};
8+
9+
use conduit::{Body, HandlerResult};
10+
use reqwest::blocking::Client;
711

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

1419
impl Default for EmberIndexRewrite {
1520
fn default() -> EmberIndexRewrite {
16-
EmberIndexRewrite { handler: None }
21+
let fastboot_client = match dotenv::var("USE_FASTBOOT") {
22+
Ok(val) if val == "staging-experimental" => Some(Client::new()),
23+
_ => None,
24+
};
25+
26+
EmberIndexRewrite {
27+
handler: None,
28+
fastboot_client,
29+
}
1730
}
1831
}
1932

@@ -24,22 +37,65 @@ impl AroundMiddleware for EmberIndexRewrite {
2437
}
2538

2639
impl Handler for EmberIndexRewrite {
27-
fn call(&self, req: &mut dyn RequestExt) -> AfterResult {
28-
// If the client is requesting html, then we've only got one page so
29-
// rewrite the request.
30-
let wants_html = req
31-
.headers()
32-
.get_all(header::ACCEPT)
33-
.iter()
34-
.any(|val| val.to_str().unwrap_or_default().contains("html"));
35-
// If the route starts with /api, just assume they want the API
36-
// response and fall through.
37-
let is_api_path = req.path().starts_with("/api");
40+
fn call(&self, req: &mut dyn RequestExt) -> HandlerResult {
3841
let handler = self.handler.as_ref().unwrap();
39-
if wants_html && !is_api_path {
40-
handler.call(&mut RequestProxy::rewrite_path(req, "/index.html"))
41-
} else {
42+
43+
if req.path().starts_with("/api") {
4244
handler.call(req)
45+
} else {
46+
if let Some(client) = &self.fastboot_client {
47+
// During local fastboot development, forward requests to the local fastboot server.
48+
// In prodution, including when running with fastboot, nginx proxies the requests
49+
// to the correct endpoint and requests should never make it here.
50+
return proxy_to_fastboot(client, req).map_err(box_error);
51+
}
52+
53+
if req
54+
.headers()
55+
.get_all(header::ACCEPT)
56+
.iter()
57+
.any(|val| val.to_str().unwrap_or_default().contains("html"))
58+
{
59+
// Serve static Ember page to bootstrap the frontend
60+
handler.call(&mut RequestProxy::rewrite_path(req, "/index.html"))
61+
} else {
62+
// Return a 404 to crawlers that don't send `Accept: text/hml`.
63+
// This is to preserve legacy behavior and will likely change.
64+
// Most of these crawlers probably won't execute our frontend JS anyway, but
65+
// it would be nice to bootstrap the app for crawlers that do execute JS.
66+
Ok(NotFound.into())
67+
}
4368
}
4469
}
4570
}
71+
72+
/// Proxy to the fastboot server in development mode
73+
///
74+
/// This handler is somewhat hacky, and is not intended for usage in production.
75+
///
76+
/// # Panics
77+
///
78+
/// This function can panic and should only be used in development mode.
79+
fn proxy_to_fastboot(client: &Client, req: &mut dyn RequestExt) -> Result<AppResponse, Error> {
80+
if req.method() != conduit::Method::GET {
81+
return Err(format!("Only support GET but request method was {}", req.method()).into());
82+
}
83+
84+
let mut url = format!("http://127.0.0.1:9000{}", req.path());
85+
if let Some(query) = req.query_string() {
86+
write!(url, "?{}", query).map_err(|e| e.to_string())?;
87+
}
88+
let mut fastboot_response = client
89+
.request(req.method().into(), &*url)
90+
.headers(req.headers().clone())
91+
.send()?;
92+
let mut body = Vec::new();
93+
fastboot_response.copy_to(&mut body)?;
94+
95+
let mut builder = Response::builder().status(fastboot_response.status());
96+
builder
97+
.headers_mut()
98+
.unwrap()
99+
.extend(fastboot_response.headers().clone());
100+
builder.body(Body::from_vec(body)).map_err(Into::into)
101+
}

src/util/errors.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,15 @@ impl AppError for InternalAppError {
226226
#[derive(Debug, Clone, Copy)]
227227
pub struct NotFound;
228228

229+
impl From<NotFound> for AppResponse {
230+
fn from(_: NotFound) -> AppResponse {
231+
json_error("Not Found", StatusCode::NOT_FOUND)
232+
}
233+
}
234+
229235
impl AppError for NotFound {
230236
fn response(&self) -> Option<AppResponse> {
231-
Some(json_error("Not Found", StatusCode::NOT_FOUND))
237+
Some(NotFound.into())
232238
}
233239
}
234240

src/util/errors/concrete.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub enum Error {
55
DbConnect(diesel::result::ConnectionError),
66
DbQuery(diesel::result::Error),
77
DotEnv(dotenv::Error),
8+
Http(http::Error),
89
Internal(String),
910
Io(io::Error),
1011
JobEnqueue(swirl::EnqueueError),
@@ -20,6 +21,7 @@ impl fmt::Display for Error {
2021
Error::DbConnect(inner) => inner.fmt(f),
2122
Error::DbQuery(inner) => inner.fmt(f),
2223
Error::DotEnv(inner) => inner.fmt(f),
24+
Error::Http(inner) => inner.fmt(f),
2325
Error::Internal(inner) => inner.fmt(f),
2426
Error::Io(inner) => inner.fmt(f),
2527
Error::JobEnqueue(inner) => inner.fmt(f),
@@ -47,6 +49,12 @@ impl From<dotenv::Error> for Error {
4749
}
4850
}
4951

52+
impl From<http::Error> for Error {
53+
fn from(err: http::Error) -> Self {
54+
Error::Http(err)
55+
}
56+
}
57+
5058
impl From<String> for Error {
5159
fn from(err: String) -> Self {
5260
Error::Internal(err)

0 commit comments

Comments
 (0)