Skip to content

Commit 095d1fa

Browse files
authored
feat: 12-factor env vars FLAPI_PORT + FLAPI_HOST (#63) (#67)
- main.cpp: FLAPI_PORT / FLAPI_HOST fallback for --port / --host with CLI > env > config > default precedence; invalid FLAPI_PORT (non-int / out of range) exits 1 with single-line stderr, mirroring the FLAPI_LOG_LEVEL pattern from #47 - main.cpp: new --host CLI flag, paired with FLAPI_HOST - config_manager: http-host YAML key (default 0.0.0.0) + get/setHttpHost accessors - api_server: pass bindaddr to crow so the configured host is honoured - test_env_overrides.py: 7 new pytest cases covering invalid FLAPI_PORT (abc / 0 / 99999 / -1), env-only port bind, CLI-wins-over-env precedence, and FLAPI_HOST bind - CLI_REFERENCE / CONFIG_REFERENCE / AGENTS: docs reflect the new env vars and the http-host config field
1 parent db5ff79 commit 095d1fa

8 files changed

Lines changed: 259 additions & 16 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,8 @@ rate_limit:
771771
# Configuration
772772
FLAPI_CONFIG=path/to/flapi.yaml # Config file path
773773
FLAPI_LOG_LEVEL=debug|info|warn|error
774+
FLAPI_PORT=8080 # HTTP port (fallback for --port)
775+
FLAPI_HOST=0.0.0.0 # Bind address (fallback for --host)
774776
775777
# Authentication
776778
JWT_SECRET=your-secret-key # JWT signing key

docs/CLI_REFERENCE.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This document provides a complete reference for the `flapi` server executable's
1515
2. [Command-Line Options](#2-command-line-options)
1616
- [Configuration File](#configuration-file---c---config)
1717
- [Server Port](#server-port---p---port)
18+
- [Bind Host](#bind-host---host)
1819
- [Log Level](#log-level---log-level)
1920
- [Validate Configuration](#validate-configuration---validate-config)
2021
- [Configuration Service](#configuration-service---config-service)
@@ -134,11 +135,22 @@ Overrides the HTTP server port defined in the configuration file.
134135
| Type | integer |
135136
| Default | From config file (typically `8080`) |
136137
| Required | No |
138+
| Environment variable | `FLAPI_PORT` |
137139

138140
**Description:**
139141

140142
When specified, this option overrides the `http-port` value in the configuration file. Useful for running multiple instances or when port configuration needs to be dynamic.
141143

144+
**Precedence (highest wins):**
145+
1. `-p` / `--port` CLI flag
146+
2. `FLAPI_PORT` environment variable
147+
3. `http-port` from `flapi.yaml`
148+
4. Built-in default (`8080`)
149+
150+
Invalid `FLAPI_PORT` values (non-integer, `<1`, `>65535`) cause flapi to
151+
exit with a single-line error -- a typo like `FLAPI_PORT=abc` surfaces
152+
immediately rather than silently falling through to the config-file value.
153+
142154
**Example:**
143155

144156
```bash
@@ -147,9 +159,52 @@ When specified, this option overrides the `http-port` value in the configuration
147159

148160
# Override config file port
149161
./flapi -c production.yaml --port 80
162+
163+
# 12-factor: pick up the port from the environment
164+
export FLAPI_PORT=9000
165+
./flapi
166+
```
167+
168+
> **Implementation:** `src/main.cpp`, `src/api_server.cpp` | **Tests:** `test/integration/test_env_overrides.py`, `test/integration/conftest.py`
169+
170+
---
171+
172+
### Bind Host (`--host`)
173+
174+
Overrides the bind address (`http-host`) defined in the configuration file.
175+
176+
| Property | Value |
177+
|----------|-------|
178+
| Long form | `--host` |
179+
| Type | string |
180+
| Default | From config file (`0.0.0.0` if unset) |
181+
| Required | No |
182+
| Environment variable | `FLAPI_HOST` |
183+
184+
**Description:**
185+
186+
Controls which network interface the HTTP server binds on. Use
187+
`127.0.0.1` to restrict access to the loopback interface only, or
188+
`0.0.0.0` to accept connections on all interfaces.
189+
190+
**Precedence (highest wins):**
191+
1. `--host` CLI flag
192+
2. `FLAPI_HOST` environment variable
193+
3. `http-host` from `flapi.yaml`
194+
4. Built-in default (`0.0.0.0`)
195+
196+
**Example:**
197+
198+
```bash
199+
# Loopback only (useful behind a reverse proxy)
200+
./flapi --host 127.0.0.1
201+
202+
# 12-factor: pick up the host from the environment
203+
export FLAPI_HOST=127.0.0.1
204+
./flapi
150205
```
151206

152-
> **Implementation:** `src/main.cpp`, `src/api_server.cpp` | **Tests:** `test/integration/conftest.py`
207+
> **Implementation:** `src/main.cpp`, `src/api_server.cpp`, `src/config_manager.cpp` | **Tests:** `test/integration/test_env_overrides.py`
153208
154209
---
155210

@@ -520,6 +575,8 @@ notarisation.
520575
| Variable | Description | Used By |
521576
|----------|-------------|---------|
522577
| `FLAPI_CONFIG` | Path to `flapi.yaml` (fallback for `-c`) | `--config` fallback |
578+
| `FLAPI_PORT` | HTTP server port (fallback for `-p` / `--port`); invalid values exit 1 | `--port` fallback |
579+
| `FLAPI_HOST` | Bind address (fallback for `--host`) | `--host` fallback |
523580
| `FLAPI_LOG_LEVEL` | Log verbosity (fallback for `--log-level`); invalid values exit 1 | `--log-level` fallback |
524581
| `FLAPI_CONFIG_SERVICE_TOKEN` | Authentication token for configuration service API | `--config-service-token` fallback |
525582
| `FLAPI_NO_TELEMETRY` | Disable telemetry when set to `1`, `true`, or `yes` | `--no-telemetry` fallback |

docs/CONFIG_REFERENCE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ should not bake in.
151151
| Env var | Read at | Effect | Precedence |
152152
|---------|---------|--------|------------|
153153
| `FLAPI_CONFIG` | startup | Path to `flapi.yaml` (fallback for `-c`) | CLI > env > `flapi.yaml` default |
154+
| `FLAPI_PORT` | startup | HTTP server port (fallback for `-p` / `--port`) | CLI > env > `http-port` config > `8080`; invalid values exit non-zero |
155+
| `FLAPI_HOST` | startup | Bind address (fallback for `--host`) | CLI > env > `http-host` config > `0.0.0.0` |
154156
| `FLAPI_LOG_LEVEL` | startup | Log verbosity (fallback for `--log-level`) | CLI > env > `info` default; invalid values exit non-zero |
155157
| `FLAPI_CONFIG_SERVICE_TOKEN` | startup | Bearer token for the management API (fallback for `--config-service-token`) | CLI > env > auto-generate |
156158
| `FLAPI_NO_TELEMETRY` | startup | Disable PostHog telemetry (fallback for `--no-telemetry`) | CLI > env > config-file > enabled |
@@ -178,14 +180,16 @@ The main configuration file defines global settings, connections, and server beh
178180
| `project-name` | string | - | Human-readable project name |
179181
| `project-description` | string | - | Project description |
180182
| `server-name` | string | `"localhost"` | Server hostname for generated URLs |
181-
| `http-port` | integer | `8080` | HTTP server port |
183+
| `http-port` | integer | `8080` | HTTP server port (overridable via `--port` / `FLAPI_PORT`) |
184+
| `http-host` | string | `"0.0.0.0"` | Bind address (overridable via `--host` / `FLAPI_HOST`); use `127.0.0.1` to restrict to loopback |
182185

183186
**Example:**
184187

185188
```yaml
186189
project-name: Customer API
187190
project-description: REST API for customer data access
188191
server-name: api.example.com
192+
http-host: 0.0.0.0
189193
http-port: 8080
190194
```
191195
@@ -1808,6 +1812,7 @@ flAPI supports both hyphenated and camelCase naming for backward compatibility:
18081812
| Configuration | Default Value |
18091813
|---------------|---------------|
18101814
| `http-port` | `8080` |
1815+
| `http-host` | `"0.0.0.0"` |
18111816
| `server-name` | `"localhost"` |
18121817
| `duckdb.db_path` | `:memory:` |
18131818
| `duckdb.access_mode` | `READ_WRITE` |

src/api_server.cpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,19 +283,22 @@ void APIServer::run(int port) {
283283
}
284284

285285
const auto& https = configManager->getHttpsConfig();
286+
const std::string bind_host = configManager->getHttpHost();
286287
if (https.enabled) {
287-
CROW_LOG_INFO << "HTTPS enabled: serving TLS on port " << configManager->getHttpPort();
288+
CROW_LOG_INFO << "HTTPS enabled: serving TLS on " << bind_host << ":" << configManager->getHttpPort();
288289
CROW_LOG_DEBUG << " cert: " << https.ssl_cert_file;
289290
CROW_LOG_DEBUG << " key: " << https.ssl_key_file;
290-
app.port(configManager->getHttpPort())
291+
app.bindaddr(bind_host)
292+
.port(configManager->getHttpPort())
291293
.server_name("flAPI")
292294
.multithreaded()
293295
.use_compression(crow::compression::GZIP)
294296
.ssl_file(https.ssl_cert_file, https.ssl_key_file)
295297
.run();
296298
} else {
297-
CROW_LOG_INFO << "Server starting on port " << configManager->getHttpPort() << "...";
298-
app.port(configManager->getHttpPort())
299+
CROW_LOG_INFO << "Server starting on " << bind_host << ":" << configManager->getHttpPort() << "...";
300+
app.bindaddr(bind_host)
301+
.port(configManager->getHttpPort())
299302
.server_name("flAPI")
300303
.multithreaded()
301304
.use_compression(crow::compression::GZIP)

src/config_manager.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,11 @@ void ConfigManager::parseMainConfig() {
109109
project_description = safeGet<std::string>(config, "project-description", "project-description");
110110
server_name = safeGet<std::string>(config, "server-name", "server-name", "localhost");
111111
http_port = safeGet<int>(config, "http-port", "http-port", 8080);
112+
http_host = safeGet<std::string>(config, "http-host", "http-host", "0.0.0.0");
112113

113114
CROW_LOG_DEBUG << "Project Name: " << project_name;
114115
CROW_LOG_DEBUG << "Server Name: " << server_name;
116+
CROW_LOG_DEBUG << "HTTP Host: " << http_host;
115117
CROW_LOG_DEBUG << "HTTP Port: " << http_port;
116118

117119
parseHttpsConfig();
@@ -1184,6 +1186,8 @@ std::string ConfigManager::getProjectDescription() const { return project_descri
11841186
std::string ConfigManager::getServerName() const { return server_name; }
11851187
int ConfigManager::getHttpPort() const { return http_port; }
11861188
void ConfigManager::setHttpPort(int port) { http_port = port; }
1189+
std::string ConfigManager::getHttpHost() const { return http_host; }
1190+
void ConfigManager::setHttpHost(const std::string& host) { http_host = host; }
11871191
std::string ConfigManager::getTemplatePath() const { return template_config.path; }
11881192
std::filesystem::path ConfigManager::getFullTemplatePath() const { return std::filesystem::path(base_path) / template_config.path; }
11891193
std::shared_ptr<IFileProvider> ConfigManager::getFileProvider() const {

src/include/config_manager.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,8 @@ class ConfigManager {
526526
std::string getServerName() const;
527527
int getHttpPort() const;
528528
void setHttpPort(int port);
529+
std::string getHttpHost() const;
530+
void setHttpHost(const std::string& host);
529531
virtual std::string getTemplatePath() const;
530532
std::string getCacheSchema() const;
531533
const std::unordered_map<std::string, ConnectionConfig>& getConnections() const;
@@ -606,6 +608,7 @@ class ConfigManager {
606608
std::string cache_schema = "flapi";
607609
std::string server_name;
608610
int http_port = 8080;
611+
std::string http_host = "0.0.0.0";
609612
std::unordered_map<std::string, ConnectionConfig> connections;
610613
RateLimitConfig rate_limit_config;
611614
bool auth_enabled;

src/main.cpp

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <fstream>
44
#include <iostream>
55
#include <cstdlib>
6+
#include <cstring>
67
#include <csignal>
78
#include <atomic>
89
#include <thread>
@@ -322,6 +323,10 @@ int main(int argc, char* argv[])
322323
.default_value(-1)
323324
.scan<'i', int>();
324325

326+
program.add_argument("--host")
327+
.help("Bind address for the web server (e.g. 0.0.0.0, 127.0.0.1)")
328+
.default_value(std::string(""));
329+
325330
program.add_argument("--log-level")
326331
.help("Set the log level (debug, info, warning, error)")
327332
.default_value(std::string("info"));
@@ -439,13 +444,16 @@ int main(int argc, char* argv[])
439444

440445
std::string config_file = program.get<std::string>("--config");
441446
int cmd_port = program.get<int>("--port");
447+
std::string cmd_host = program.get<std::string>("--host");
442448
std::string log_level = program.get<std::string>("--log-level");
443449
bool validate_config = program.get<bool>("--validate-config");
444450

445-
// 12-factor env-var fallback (#47). Precedence:
446-
// CLI flag > env var > built-in default.
451+
// 12-factor env-var fallback (#47, #63). Precedence:
452+
// CLI flag > env var > config file > built-in default.
447453
// CLI wins because we only consult the env when the user didn't
448-
// pass the flag.
454+
// pass the flag; config-file values are applied later in
455+
// initializeConfig() and only kick in when neither CLI nor env
456+
// provided a value.
449457
if (!program.is_used("--config")) {
450458
if (const char* env = std::getenv("FLAPI_CONFIG"); env != nullptr && *env != '\0') {
451459
config_file = env;
@@ -456,6 +464,29 @@ int main(int argc, char* argv[])
456464
log_level = env;
457465
}
458466
}
467+
if (!program.is_used("--port")) {
468+
if (const char* env = std::getenv("FLAPI_PORT"); env != nullptr && *env != '\0') {
469+
// Reject non-int / out-of-range early so a typo doesn't
470+
// silently fall through to the config-file value.
471+
try {
472+
size_t consumed = 0;
473+
const int parsed = std::stoi(env, &consumed);
474+
if (consumed != std::strlen(env) || parsed < 1 || parsed > 65535) {
475+
throw std::invalid_argument("out of range");
476+
}
477+
cmd_port = parsed;
478+
} catch (const std::exception&) {
479+
std::cerr << "flapi: invalid FLAPI_PORT '" << env
480+
<< "'; must be an integer in 1..65535\n";
481+
return 1;
482+
}
483+
}
484+
}
485+
if (!program.is_used("--host")) {
486+
if (const char* env = std::getenv("FLAPI_HOST"); env != nullptr && *env != '\0') {
487+
cmd_host = env;
488+
}
489+
}
459490
// Validate log_level. Invalid values are an error, not a silent
460491
// fallback -- typos like FLAPI_LOG_LEVEL=DEBUG should surface
461492
// immediately, not run the server at the wrong verbosity.
@@ -519,6 +550,9 @@ int main(int argc, char* argv[])
519550
if (cmd_port != -1) {
520551
config_manager->setHttpPort(cmd_port);
521552
}
553+
if (!cmd_host.empty()) {
554+
config_manager->setHttpHost(cmd_host);
555+
}
522556

523557
initializeDatabase(config_manager);
524558

0 commit comments

Comments
 (0)