Skip to content
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ name: CI
on:
push:
branches: ["main"]
paths-ignore:
- '**/*.md'
pull_request:
paths-ignore:
- '**/*.md'

permissions:
contents: read
Expand Down
29 changes: 27 additions & 2 deletions buildpacks/static-web-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ For example, a request to `example.com/support` will be tried in the document ro

*Default: none*

Define preset HTTP responses. Supports the common HTTP redirect use-case, or any custom status, headers, and body response.
Define preset HTTP responses. Supports common use-cases such as: HTTP redirects, health check endpoint, or any custom status, headers, and body response.

```toml
[[com.heroku.static-web-server.caddy_server_opts.static_responses]]
Expand Down Expand Up @@ -352,7 +352,9 @@ body = "I could be anything."
- `{http.request.uri.path.file}`
- `{http.request.uri.query}`

For example, permanently redirect to a different path with status `301` and a `Location` header, using the requested filename in the new path:
##### Caddy: Redirect examples

Permanently redirect to a different path with status `301` and a `Location` header, using the requested filename in the new path:

```toml
[[com.heroku.static-web-server.caddy_server_opts.static_responses]]
Expand All @@ -373,6 +375,16 @@ status = 301
"X-Redirected-From" = "original.example.com"
```

##### Caddy: Health check example

```toml
[[com.heroku.static-web-server.caddy_server_opts.static_responses]]
path_matcher = "/health"
body = '{"status":"ok"}'
[com.heroku.static-web-server.caddy_server_opts.static_responses.headers]
"Content-Type" = "application/json"
```

#### Caddy: Basic Authorization

*Default: not enabled*
Expand Down Expand Up @@ -539,3 +551,16 @@ docker run \
-it --entrypoint bash \
<APP_NAME>
```

## Route Precendence

The static web server is configured to handle request URLs with the following path-matched precedence:

1. [optional] [Caddy: Basic Authorization](#caddy-basic-authorization)
2. [optional] [Caddy: Static Responses](#caddy-static-responses) (terminating)
3. [optional] [Caddy: Clean URLs](#caddy-clean-urls)
1. exact URL path
2. URL path + `.html` (rewrite)
4. File Server
1. exact URL path
2. for directories, URL path + default document `index.html`
148 changes: 101 additions & 47 deletions buildpacks/static-web-server/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ fn default_behavior() {
&mut ContainerConfig::new(),
|_container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}")).call()
ureq::get(&format!("http://{socket_addr}"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand Down Expand Up @@ -48,7 +50,9 @@ fn build_command() {
|_container, socket_addr| {
// Test for successful response
let response = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/")).call()
ureq::get(&format!("http://{socket_addr}/"))
.call()
.map_err(Box::new)
})
.unwrap();
let response_status = response.status();
Expand All @@ -61,7 +65,9 @@ fn build_command() {

// Test for default Not Found response
let response = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/test-output.txt")).call()
ureq::get(&format!("http://{socket_addr}/test-output.txt"))
.call()
.map_err(Box::new)
})
.unwrap();
let response_status = response.status();
Expand Down Expand Up @@ -101,7 +107,9 @@ fn top_level_doc_root() {
&mut ContainerConfig::new(),
|_container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/")).call()
ureq::get(&format!("http://{socket_addr}/"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand All @@ -120,22 +128,26 @@ fn top_level_doc_root() {
}

let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/non-existent-path")).call()
ureq::get(&format!("http://{socket_addr}/non-existent-path"))
.call()
.map_err(Box::new)
});
match response_result {
Err(ureq::Error::Status(code, response)) => {
assert_eq!(code, 404);
let h = response.header("Content-Type").unwrap_or_default();
assert_contains!(h, "text/html");
let response_body = response.into_string().unwrap();
assert_contains!(response_body, "Custom 404");
}
Ok(_) => {
panic!("should respond 404 Not Found, but got 200 ok");
}
Err(error) => {
panic!("should respond 404 Not Found, but got other error: {error:?}");
}
Err(err) => match *err {
ureq::Error::Status(code, response) => {
assert_eq!(code, 404);
let h = response.header("Content-Type").unwrap_or_default();
assert_contains!(h, "text/html");
let response_body = response.into_string().unwrap();
assert_contains!(response_body, "Custom 404");
}
error @ ureq::Error::Transport(_) => {
panic!("should respond 404 Not Found, but got other error: {error:?}");
}
},
}
},
);
Expand All @@ -152,7 +164,9 @@ fn custom_headers() {
&mut ContainerConfig::new(),
|_container, socket_addr| {
let response = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/")).call()
ureq::get(&format!("http://{socket_addr}/"))
.call()
.map_err(Box::new)
})
.unwrap();
let h = response.header("X-Global").unwrap_or_default();
Expand All @@ -167,7 +181,9 @@ fn custom_headers() {
);

let response = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/page2.html")).call()
ureq::get(&format!("http://{socket_addr}/page2.html"))
.call()
.map_err(Box::new)
})
.unwrap();
let h = response.header("X-Only-HTML").unwrap_or_default();
Expand All @@ -193,22 +209,26 @@ fn custom_errors() {
&mut ContainerConfig::new(),
|_container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/non-existent-path")).call()
ureq::get(&format!("http://{socket_addr}/non-existent-path"))
.call()
.map_err(Box::new)
});
match response_result {
Err(ureq::Error::Status(code, response)) => {
assert_eq!(code, 404);
let h = response.header("Content-Type").unwrap_or_default();
assert_contains!(h, "text/html");
let response_body = response.into_string().unwrap();
assert_contains!(response_body, "Custom 404");
}
Ok(_) => {
panic!("should respond 404 Not Found, but got 200 ok");
}
Err(error) => {
panic!("should respond 404 Not Found, but got other error: {error:?}");
}
Err(err) => match *err {
ureq::Error::Status(code, response) => {
assert_eq!(code, 404);
let h = response.header("Content-Type").unwrap_or_default();
assert_contains!(h, "text/html");
let response_body = response.into_string().unwrap();
assert_contains!(response_body, "Custom 404");
}
error @ ureq::Error::Transport(_) => {
panic!("should respond 404 Not Found, but got other error: {error:?}");
}
},
}
},
);
Expand All @@ -225,7 +245,9 @@ fn client_side_routing() {
&mut ContainerConfig::new(),
|_container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/non-existent-path")).call()
ureq::get(&format!("http://{socket_addr}/non-existent-path"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand Down Expand Up @@ -260,7 +282,9 @@ fn runtime_configuration_custom() {
),
|container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/")).call()
ureq::get(&format!("http://{socket_addr}/"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand All @@ -275,7 +299,9 @@ fn runtime_configuration_custom() {
}
}
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/subsection/")).call()
ureq::get(&format!("http://{socket_addr}/subsection/"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand Down Expand Up @@ -333,7 +359,9 @@ fn runtime_configuration_default() {
),
|_container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/")).call()
ureq::get(&format!("http://{socket_addr}/"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand Down Expand Up @@ -362,7 +390,9 @@ fn caddy_csp_nonce() {
&mut ContainerConfig::new(),
|container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}")).call()
ureq::get(&format!("http://{socket_addr}"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand Down Expand Up @@ -395,7 +425,9 @@ fn caddy_clean_urls() {
&mut ContainerConfig::new(),
|container, socket_addr| {
let index_response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}")).call()
ureq::get(&format!("http://{socket_addr}"))
.call()
.map_err(Box::new)
});
match index_response_result {
Ok(response) => {
Expand All @@ -410,7 +442,9 @@ fn caddy_clean_urls() {
}
}
let other_response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/other")).call()
ureq::get(&format!("http://{socket_addr}/other"))
.call()
.map_err(Box::new)
});
match other_response_result {
Ok(response) => {
Expand All @@ -425,7 +459,9 @@ fn caddy_clean_urls() {
}
}
let nested_response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/nested")).call()
ureq::get(&format!("http://{socket_addr}/nested"))
.call()
.map_err(Box::new)
});
match nested_response_result {
Ok(response) => {
Expand All @@ -441,7 +477,9 @@ fn caddy_clean_urls() {
}
let nested_second_response_result =
retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/nested/second")).call()
ureq::get(&format!("http://{socket_addr}/nested/second"))
.call()
.map_err(Box::new)
});
match nested_second_response_result {
Ok(response) => {
Expand All @@ -457,7 +495,9 @@ fn caddy_clean_urls() {
}
let nested_deeper_response_result =
retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}/nested/deeper")).call()
ureq::get(&format!("http://{socket_addr}/nested/deeper"))
.call()
.map_err(Box::new)
});
match nested_deeper_response_result {
Ok(response) => {
Expand Down Expand Up @@ -486,7 +526,9 @@ fn caddy_access_logs() {
&mut ContainerConfig::new(),
|container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}")).call()
ureq::get(&format!("http://{socket_addr}"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand Down Expand Up @@ -522,17 +564,23 @@ fn caddy_basic_auth() {
),
|container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}")).call()
ureq::get(&format!("http://{socket_addr}"))
.call()
.map_err(Box::new)
});
match response_result {
Err(ureq::Error::Status(code, _response)) => {
assert_eq!(code, 401);
}
Ok(_) => {
panic!("should respond 401 Unauthorized, but got 200 ok");
}
Err(error) => {
panic!("should respond 401 Unauthorized, but got other error: {error:?}");
Err(err) => {
match *err {
ureq::Error::Status(code, _response) => {
assert_eq!(code, 401);
}
error @ ureq::Error::Transport(_) => {
panic!("should respond 401 Unauthorized, but got other error: {error:?}");
}
}
}
}

Expand All @@ -545,6 +593,7 @@ fn caddy_basic_auth() {
"Basic dmlzaXRvcjpvcGVuc2Vhc2FtZQ==",
)
.call()
.map_err(Box::new)
});
match auth_response_result {
Ok(response) => {
Expand Down Expand Up @@ -579,7 +628,9 @@ fn caddy_basic_auth() {
),
|container, socket_addr| {
let response_result = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq::get(&format!("http://{socket_addr}")).call()
ureq::get(&format!("http://{socket_addr}"))
.call()
.map_err(Box::new)
});
match response_result {
Ok(response) => {
Expand Down Expand Up @@ -616,7 +667,10 @@ fn caddy_static_responses() {
let ureq_agent: ureq::Agent = ureq::AgentBuilder::new().redirects(0).build();

let response = retry(DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, || {
ureq_agent.get(&format!("http://{socket_addr}")).call()
ureq_agent
.get(&format!("http://{socket_addr}"))
.call()
.map_err(Box::new)
})
.unwrap();
assert_eq!(response.status(), 200);
Expand Down
Loading
Loading