Skip to content

Commit 268b2c6

Browse files
committed
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`).
1 parent 9881593 commit 268b2c6

File tree

3 files changed

+96
-16
lines changed

3 files changed

+96
-16
lines changed

src/middleware.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod prelude {
55
use std::error::Error;
66
pub type BoxError = Box<dyn Error + Send>;
77
pub type Result<T> = std::result::Result<T, BoxError>;
8+
pub type ConcreteResult<T> = std::result::Result<T, crate::util::Error>;
89
}
910

1011
pub use prelude::Result;

src/middleware/ember_index_rewrite.rs

+88-15
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,34 @@
22
//! with "/api" and the Accept header contains "html".
33
44
use super::prelude::*;
5+
use std::{
6+
collections::{hash_map::Entry, HashMap},
7+
fmt::Write,
8+
io::Cursor,
9+
};
510

6-
use crate::util::RequestProxy;
11+
use crate::util::{errors::NotFound, RequestProxy};
12+
13+
use reqwest::blocking::Client;
714

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

1422
impl Default for EmberIndexRewrite {
1523
fn default() -> EmberIndexRewrite {
16-
EmberIndexRewrite { handler: None }
24+
let fastboot_client = match dotenv::var("USE_FASTBOOT") {
25+
Ok(val) if val == "staging-experimental" => Some(Client::new()),
26+
_ => None,
27+
};
28+
29+
EmberIndexRewrite {
30+
handler: None,
31+
fastboot_client,
32+
}
1733
}
1834
}
1935

@@ -25,21 +41,78 @@ impl AroundMiddleware for EmberIndexRewrite {
2541

2642
impl Handler for EmberIndexRewrite {
2743
fn call(&self, req: &mut dyn Request) -> Result<Response> {
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-
.find("Accept")
33-
.map(|accept| accept.iter().any(|s| s.contains("html")))
34-
.unwrap_or(false);
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");
3844
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 {
45+
46+
if req.path().starts_with("/api") {
4247
handler.call(req)
48+
} else {
49+
if let Some(client) = &self.fastboot_client {
50+
// During local fastboot development, forward requests to the local fastboot server.
51+
// In prodution, including when running with fastboot, nginx proxies the requests
52+
// to the correct endpoint and requests should never make it here.
53+
return proxy_to_fastboot(client, req).map_err(|e| Box::new(e) as BoxError);
54+
}
55+
56+
if req
57+
.headers()
58+
.find("Accept")
59+
.map(|accept| accept.iter().any(|s| s.contains("html")))
60+
.unwrap_or(false)
61+
{
62+
// Serve static Ember page to bootstrap the frontend
63+
handler.call(&mut RequestProxy::rewrite_path(req, "/index.html"))
64+
} else {
65+
// Return a 404 to crawlers that don't send `Accept: text/hml`.
66+
// This is to preserve legacy behavior and will likely change.
67+
// Most of these crawlers probably won't execute our frontend JS anyway, but
68+
// it would be nice to bootstrap the app for crawlers that do execute JS.
69+
Ok(NotFound.into())
70+
}
4371
}
4472
}
4573
}
74+
75+
/// Proxy to the fastboot server in development mode
76+
///
77+
/// This handler is somewhat hacky, and is not intended for usage in production.
78+
///
79+
/// # Panics
80+
///
81+
/// This function can panic and should only be used in development mode.
82+
fn proxy_to_fastboot(client: &Client, req: &mut dyn Request) -> ConcreteResult<Response> {
83+
if req.method() != conduit::Method::Get {
84+
return Err(format!("Only support GET but request method was {}", req.method()).into());
85+
}
86+
87+
let mut url = format!("http://127.0.0.1:9000{}", req.path());
88+
if let Some(query) = req.query_string() {
89+
write!(url, "?{}", query).map_err(|e| e.to_string())?;
90+
}
91+
let mut request = client.request(reqwest::Method::GET, &*url);
92+
for header in req.headers().all() {
93+
for value in header.1 {
94+
request = request.header(header.0, value);
95+
}
96+
}
97+
98+
let mut fastboot_response = request.send()?;
99+
let mut headers: HashMap<String, Vec<String>> = HashMap::new();
100+
for header in fastboot_response.headers() {
101+
match headers.entry(header.0.to_string()) {
102+
Entry::Occupied(mut v) => {
103+
v.get_mut().push(header.1.to_str().unwrap().to_owned());
104+
}
105+
Entry::Vacant(v) => {
106+
v.insert(vec![header.1.to_str().unwrap().to_owned()]);
107+
}
108+
};
109+
}
110+
111+
let mut body = Vec::new();
112+
fastboot_response.copy_to(&mut body)?;
113+
Ok(conduit::Response {
114+
status: (fastboot_response.status().as_u16() as u32, "OK"),
115+
headers,
116+
body: Box::new(Cursor::new(body)),
117+
})
118+
}

src/util/errors.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,15 @@ impl AppError for InternalAppError {
221221
#[derive(Debug, Clone, Copy)]
222222
pub struct NotFound;
223223

224+
impl From<NotFound> for Response {
225+
fn from(_: NotFound) -> Response {
226+
json_error("Not Found", (404, "Not Found"))
227+
}
228+
}
229+
224230
impl AppError for NotFound {
225231
fn response(&self) -> Option<Response> {
226-
Some(json_error("Not Found", (404, "Not Found")))
232+
Some(NotFound.into())
227233
}
228234
}
229235

0 commit comments

Comments
 (0)