Skip to content

Feature add env#425

Open
rousbound wants to merge 10 commits intoferronweb:develop-2.xfrom
rousbound:feature-add-ENV
Open

Feature add env#425
rousbound wants to merge 10 commits intoferronweb:develop-2.xfrom
rousbound:feature-add-ENV

Conversation

@rousbound
Copy link
Copy Markdown

@rousbound rousbound commented Jan 17, 2026

Hi @DorianNiemiecSVRJS!

I've created this PR as a follow up of the discussion on issue #233 and the PR #236 .

I tried to follow the style of the previous PR on lookup_env_var.rs with the addition of:

  • Changes to kdl.rs adapter
  • Interpolation of env variables in lookup_env_var.rs

I appreciate any feedback.

Thanks in advance.

use anyhow::bail;
use std::env;

pub fn lookup_env_value(conf_string: String) -> anyhow::Result<String> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be done cleaner with the regex crate, see here for example.

Copy link
Copy Markdown
Member

@DorianNiemiecSVRJS DorianNiemiecSVRJS Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amejia1 I think procedural code is enough... Let's not over-use regexes...

use std::env;

pub fn lookup_env_value(conf_string: String) -> anyhow::Result<String> {
if !conf_string.contains("{env:") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ferron config files look a lot like caddy config files. How about using the same syntax like the caddyfile "env" placeholders? See https://caddyserver.com/docs/conventions#placeholders .

}
}

#[cfg(test)]
Copy link
Copy Markdown
Contributor

@amejia1 amejia1 Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a way to escape the placeholder, for example \{env.HOME} should render {env.HOME}.

for kdl_entry in kdl_node.iter() {
let value = match kdl_entry.value().to_owned() {
KdlValue::String(value) => ServerConfigurationValue::String(value),
KdlValue::String(value) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will probably be best to render all placeholders first and then process the config files. Will this config work?

{env.HTTP_ADDRESS} {
    proxy "http://localhost:3000/" // Replace "http://localhost:3000" with the backend server URL
}

@rousbound
Copy link
Copy Markdown
Author

rousbound commented Mar 27, 2026

Hello @amejia1! Sorry for taking so long to catch up with your comments.

I found all of our points very reasonable.

Regarding the implementation of them:

  • 1st suggestion (regex changes): straightforward to implement, but needs to add regex lib to Cargo.toml(currently there is fancy-regex)
  • 2nd suggestion (Caddy-like env syntax): more impactful since it affects Ferron’s config ergonomics, so it would be good to get alignment on this
  • 3rd suggestion (escaping of braces): also straightforward
  • 4th suggestion (render order): reasonable, though the “render first” approach might have edge cases worth validating

@DorianNiemiecSVRJS do you have any position regarding @amejia1 suggestions, specifically the env syntax and the render-order approach?

In the meantime, I’m planning to prototype these changes in this PR.

@DorianNiemiecSVRJS
Copy link
Copy Markdown
Member

@DorianNiemiecSVRJS do you have any position regarding @amejia1 suggestions, specifically the env syntax and the render-order approach?

I think procedural code (like it's in https://github.com/ferronweb/ferron/blob/48eda07699092a268382fcde041b18b7c6897ee0/ferron/src/util/log_placeholders.rs) is enough...

@rousbound
Copy link
Copy Markdown
Author

Hello again @DorianNiemiecSVRJS!

@DorianNiemiecSVRJS do you have any position regarding @amejia1 suggestions, specifically the env syntax and the render-order approach?

I think procedural code (like it's in https://github.com/ferronweb/ferron/blob/48eda07699092a268382fcde041b18b7c6897ee0/ferron/src/util/log_placeholders.rs) is enough...

I've adapted the parsing to match the style of log_placeholders.rs, could you review it?

Thanks in advance!

let value = match kdl_entry.value().to_owned() {
KdlValue::String(value) => ServerConfigurationValue::String(value),
KdlValue::String(value) => {
let resolved_value = replace_placeholders(&value).expect("Failed to resolve environment variable in configuration");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If environment variable wouldn't be found (see https://github.com/ferronweb/ferron/pull/425/changes#diff-63c5becf3cec064da1306878c6a2ffe08a0e70be83a1d6c801b59e2b268339abR8), the web server would crash... Could this be handled more gracefully, like it's in:

pub fn replace_header_placeholders(
input: &str,
request_parts: &hyper::http::request::Parts,
socket_data: Option<&SocketData>,
) -> String {
let mut output = String::new();
let mut index_rb_saved = 0;
loop {
let index_lb = input[index_rb_saved..].find("{");
if let Some(index_lb) = index_lb {
let index_rb_afterlb = input[index_rb_saved + index_lb + 1..].find("}");
if let Some(index_rb_afterlb) = index_rb_afterlb {
let index_rb = index_rb_afterlb + index_lb + 1;
let placeholder_value = &input[index_rb_saved + index_lb + 1..index_rb_saved + index_rb];
output.push_str(&input[index_rb_saved..index_rb_saved + index_lb]);
match placeholder_value {
"path" => output.push_str(request_parts.uri.path()),
"path_and_query" => output.push_str(
request_parts
.uri
.path_and_query()
.map_or(request_parts.uri.path(), |p| p.as_str()),
),
"method" => output.push_str(request_parts.method.as_str()),
"version" => output.push_str(match request_parts.version {
hyper::Version::HTTP_09 => "HTTP/0.9",
hyper::Version::HTTP_10 => "HTTP/1.0",
hyper::Version::HTTP_11 => "HTTP/1.1",
hyper::Version::HTTP_2 => "HTTP/2.0",
hyper::Version::HTTP_3 => "HTTP/3.0",
_ => "HTTP/Unknown",
}),
"scheme" => {
if let Some(socket_data) = socket_data {
output.push_str(if socket_data.encrypted { "https" } else { "http" });
} else {
// No socket data, leave it as is
output.push_str("{scheme}");
}
}
"client_ip" => {
if let Some(socket_data) = socket_data {
output.push_str(&socket_data.remote_addr.ip().to_string());
} else {
// No socket data, leave it as is
output.push_str("{client_ip}");
}
}
"client_port" => {
if let Some(socket_data) = socket_data {
output.push_str(&socket_data.remote_addr.port().to_string());
} else {
// No socket data, leave it as is
output.push_str("{client_port}");
}
}
"client_ip_canonical" => {
if let Some(socket_data) = socket_data {
output.push_str(&socket_data.remote_addr.ip().to_canonical().to_string());
} else {
// No socket data, leave it as is
output.push_str("{client_ip_canonical}");
}
}
"server_ip" => {
if let Some(socket_data) = socket_data {
output.push_str(&socket_data.local_addr.ip().to_string());
} else {
// No socket data, leave it as is
output.push_str("{server_ip}");
}
}
"server_port" => {
if let Some(socket_data) = socket_data {
output.push_str(&socket_data.local_addr.port().to_string());
} else {
// No socket data, leave it as is
output.push_str("{server_port}");
}
}
"server_ip_canonical" => {
if let Some(socket_data) = socket_data {
output.push_str(&socket_data.local_addr.ip().to_canonical().to_string());
} else {
// No socket data, leave it as is
output.push_str("{server_ip_canonical}");
}
}
_ => {
if let Some(header_name) = placeholder_value.strip_prefix("header:") {
if let Some(header_value) = request_parts.headers.get(header_name) {
output.push_str(header_value.to_str().unwrap_or(""));
}
} else {
// Unknown placeholder, leave it as is
output.push('{');
output.push_str(placeholder_value);
output.push('}');
}
}
}
if index_rb < input.len() - 1 {
index_rb_saved += index_rb + 1;
} else {
break;
}
} else {
output.push_str(&input[index_rb_saved..]);
}
} else {
output.push_str(&input[index_rb_saved..]);
break;
}
}
output
}

@DorianNiemiecSVRJS
Copy link
Copy Markdown
Member

Apologies for such a late review, I was working on a future major version of Ferron...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants