Skip to content

feat: add host binding configuration option - #920

Open
FLASH2332 wants to merge 5 commits into
OneBusAway:mainfrom
FLASH2332:feature/binding-address
Open

feat: add host binding configuration option#920
FLASH2332 wants to merge 5 commits into
OneBusAway:mainfrom
FLASH2332:feature/binding-address

Conversation

@FLASH2332

@FLASH2332 FLASH2332 commented May 10, 2026

Copy link
Copy Markdown
Contributor

Adds a host field to the configuration file to control the server's binding address, allowing operators to restrict the server to a specific interface (e.g. 127.0.0.1) instead of binding to all interfaces (0.0.0.0).

The field is optional and defaults to 0.0.0.0 to preserve existing behavior.

image

Fixes #896

@aaronbrethorst
@chrisls121

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added -host command-line flag and new host configuration field to control the server bind address.
  • Bug Fixes

    • Updated server bind behavior and adjusted expectations so the server listens on the configured interface (e.g., 0.0.0.0:<port>).
  • Documentation

    • Updated the example configuration and JSON schema to include host with a default value.

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds configurable HTTP server bind addresses through schema, JSON and application configuration types, a CLI flag, and server address construction. Empty hosts default to 0.0.0.0; examples and the server address test are updated accordingly.

Changes

Host Address Configuration

Layer / File(s) Summary
Configuration Schema Contract
config.schema.json, config.example.json
The schema defines host as a string with default 0.0.0.0; schema and example configurations include 127.0.0.1.
Configuration Data Types
internal/appconf/config.go, internal/appconf/json_config.go
Configuration types add Host, and ToAppConfig() maps the JSON host value into application configuration.
CLI Flag and Server Binding
cmd/api/main.go, cmd/api/app.go, cmd/api/app_test.go
The -host flag flows through configuration; CreateServer defaults an empty host to 0.0.0.0 and sets Addr to host:port, with the test expectation updated.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: ahmedhossamdev

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding host binding configuration support.
Linked Issues check ✅ Passed The PR adds configurable host binding in config and applies it when building the server address, matching issue #896.
Out of Scope Changes check ✅ Passed The changes stay focused on binding-address configuration, with related schema, example, CLI, and test updates.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/appconf/json_config.go (1)

37-37: ⚡ Quick win

Consider defaulting Host in setDefaults() for config-layer consistency.

Right now the schema advertises a 0.0.0.0 default, but JSON config defaults do not set it explicitly. Centralizing that default in setDefaults() keeps behavior/documentation aligned across consumers.

♻️ Suggested tweak
 func (j *JSONConfig) setDefaults() {
 	if j.Port == 0 {
 		j.Port = 4000
 	}
+	if j.Host == "" {
+		j.Host = "0.0.0.0"
+	}
 	if j.Env == "" {
 		j.Env = "development"
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/appconf/json_config.go` at line 37, The config struct's Host field
is advertised to default to "0.0.0.0" but that default isn't being applied in
setDefaults(); update the setDefaults() function to explicitly set Host =
"0.0.0.0" when Host is empty (or zero-valued) so JSON-configured instances align
with the schema/documentation—look for the Host field on the config struct and
the setDefaults() method to add this conditional assignment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/api/app.go`:
- Around line 153-157: The local variable host defined in app.go (host :=
cfg.Host with fallback to "0.0.0.0") is never used which causes a compile error;
update the server startup to use that host variable instead of cfg.Host (e.g.,
replace any direct use of cfg.Host when constructing the server address/Addr
with the local host variable), or alternatively remove the unused host variable
and implement the fallback directly where the server address is built so the
effective host value is used by the http.Server/start call.

---

Nitpick comments:
In `@internal/appconf/json_config.go`:
- Line 37: The config struct's Host field is advertised to default to "0.0.0.0"
but that default isn't being applied in setDefaults(); update the setDefaults()
function to explicitly set Host = "0.0.0.0" when Host is empty (or zero-valued)
so JSON-configured instances align with the schema/documentation—look for the
Host field on the config struct and the setDefaults() method to add this
conditional assignment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 33c06213-6379-4f77-aa5b-5fc8eaed1b20

📥 Commits

Reviewing files that changed from the base of the PR and between f89f0fd and 8addaaa.

📒 Files selected for processing (6)
  • cmd/api/app.go
  • cmd/api/main.go
  • config.example.json
  • config.schema.json
  • internal/appconf/config.go
  • internal/appconf/json_config.go

Comment thread cmd/api/app.go
fix : host reference
@FLASH2332
FLASH2332 force-pushed the feature/binding-address branch from 8d1c11c to f46490b Compare May 10, 2026 03:54
@sonarqubecloud

Copy link
Copy Markdown

@FLASH2332

Copy link
Copy Markdown
Contributor Author

Shall I update the appropriate comments and documentation for this addition ?

@FLASH2332

Copy link
Copy Markdown
Contributor Author

@aaronbrethorst @Ahmedhossamdev Should I make further changes ?

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/api/app_test.go`:
- Line 169: Add table-driven t.Run cases in the server address test around
srv.Addr, retaining the empty-host fallback case and adding an explicitly
configured Host: "127.0.0.1" case that verifies the resulting bind address;
include an IPv6 case only if the implementation supports it. Ensure each host
scenario constructs the server with its table-driven configuration and asserts
the expected address.

In `@cmd/api/app.go`:
- Line 205: Update the server address construction in the visible server
configuration to use net.JoinHostPort with host and strconv.Itoa(cfg.Port)
instead of fmt.Sprintf, preserving correct formatting for IPv4 and IPv6 hosts.
Add a test covering an IPv6 host such as ::1 and confirming the resulting
address is bracketed with the port.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c5965865-caab-4902-a06c-8b50e18d5d69

📥 Commits

Reviewing files that changed from the base of the PR and between 8addaaa and 229117a.

📒 Files selected for processing (3)
  • cmd/api/app.go
  • cmd/api/app_test.go
  • internal/appconf/config.go

Comment thread cmd/api/app_test.go

assert.NotNil(t, srv, "Server should not be nil")
assert.Equal(t, ":8080", srv.Addr, "Server address should match port")
assert.Equal(t, "0.0.0.0:8080", srv.Addr, "Server address should match port and address")

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test the explicitly configured host path.

This test covers only the empty-host fallback. Add a table-driven case for Host: "127.0.0.1" (and the IPv6 case if supported) so the new non-default binding path is verified.
As per coding guidelines, every new branch or condition must be covered by tests, with table-driven t.Run cases preferred for multiple scenarios.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/api/app_test.go` at line 169, Add table-driven t.Run cases in the server
address test around srv.Addr, retaining the empty-host fallback case and adding
an explicitly configured Host: "127.0.0.1" case that verifies the resulting bind
address; include an IPv6 case only if the implementation supports it. Ensure
each host scenario constructs the server with its table-driven configuration and
asserts the expected address.

Source: Coding guidelines

Comment thread cmd/api/app.go

srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Addr: fmt.Sprintf("%s:%d", host, cfg.Port),

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate app.go =="
fd -a 'app\.go$' . | sed 's#^\./##'

echo "== relevant lines =="
if [ -f cmd/api/app.go ]; then
  nl -ba cmd/api/app.go | sed -n '180,220p'
fi

echo "== host config usage =="
rg -n "server\.ListenAndServe|Address|Addr:|Host|CFG|config|port|8080" cmd/api app 2>/dev/null || true

echo "== Go resolver behavior for IPv6 literals =="
cat >/tmp/ipv6_addr_probe.go <<'EOF'
package main

import (
	"fmt"
	"net"
	"strconv"
)

func main() {
	for _, host := range []string{"localhost", "127.0.0.1", "::1"} {
		port := 8080
		fmt.Printf("%-9s => fmt=%q JoinHostPort=%q\n", host, fmt.Sprintf("%s:%d", host, port), net.JoinHostPort(host, strconv.Itoa(port)))
		if listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)); err != nil {
			fmt.Printf("%-9s fmt err   = %T %v\n", host, err, err)
		} else {
			listener.Close()
			fmt.Printf("%-9s fmt OK\n", host)
		}
		if listener, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))); err != nil {
			fmt.Printf("%-9s join err  = %T %v\n", host, err, err)
		} else {
			listener.Close()
			fmt.Printf("%-9s join OK\n", host)
		}
	}
}
EOF
go run /tmp/ipv6_addr_probe.go

Repository: OneBusAway/maglev

Length of output: 270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant lines =="
if [ -f cmd/api/app.go ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' cmd/api/app.go | sed -n '180,220p'
fi

echo "== host/config/address usage in cmd/api and nearby =="
rg -n "server\.ListenAndServe|Addr:|ListenAndServe|Config|host|Host|Port" cmd/api/go.mod 2>/dev/null || true

echo "== Go netaddr parser/listener probe =="
cat >/tmp/ipv6_addr_probe.go <<'EOF'
package main

import (
	"fmt"
	"net"
	"strconv"
)

func main() {
	for _, host := range []string{"localhost", "127.0.0.1", "::1"} {
		port := 8080
		fmt.Printf("%-9s fmt=%q\n", host, fmt.Sprintf("%s:%d", host, port))
		if _, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", host, port)); err != nil {
			fmt.Printf("%-9s ResolveTCPAddr err: %T %v\n", host, err, err)
		}
		if listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)); err != nil {
			fmt.Printf("%-9s Listen fmt err: %T %v\n", host, err, err)
		} else {
			listener.Close()
			fmt.Printf("%-9s Listen fmt OK\n", host)
		}
		addr := net.JoinHostPort(host, strconv.Itoa(port))
		fmt.Printf("%-9s JoinHostPort=%q\n", host, addr)
		if listener, err := net.Listen("tcp", addr); err != nil {
			fmt.Printf("%-9s Listen join err: %T %v\n", host, err, err)
		} else {
			listener.Close()
			fmt.Printf("%-9s Listen join OK\n", host)
		}
	}
}
EOF
go run /tmp/ipv6_addr_probe.go

Repository: OneBusAway/maglev

Length of output: 2943


Use net.JoinHostPort for IPv6-safe binding.

fmt.Sprintf("%s:%d", host, cfg.Port) produces ::1:8080 for an IPv6 host, which Go rejects as too many colons in address; IPv6 literals must be bracketed. Use net.JoinHostPort(host, strconv.Itoa(cfg.Port)) and add an IPv6 test case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/api/app.go` at line 205, Update the server address construction in the
visible server configuration to use net.JoinHostPort with host and
strconv.Itoa(cfg.Port) instead of fmt.Sprintf, preserving correct formatting for
IPv4 and IPv6 hosts. Add a test covering an IPv6 host such as ::1 and confirming
the resulting address is bracketed with the port.

@aaronbrethorst

Copy link
Copy Markdown
Member

Code review

Found 4 issues:

  1. Defaulting the empty host to 0.0.0.0 is not backward compatible. The previous Addr: ":port" makes Go bind the wildcard as a dual-stack IPv6 socket ([::]:port with IPV6_V6ONLY=0), which accepts both IPv4 and IPv6 clients. 0.0.0.0:port parses to an IPv4 address and binds AF_INET only, so IPv6 clients silently stop being able to reach the server — including anything hitting localhost where that resolves to ::1 first (e.g. the container HEALTHCHECK in Dockerfile). To actually preserve existing behavior the empty default should stay "", so the address remains :port.

maglev/cmd/api/app.go

Lines 152 to 157 in 229117a

func CreateServer(coreApp *app.Application, cfg appconf.Config) (*http.Server, *restapi.RestAPI) {
host := cfg.Host
if host == "" {
host = "0.0.0.0"
}

  1. fmt.Sprintf("%s:%d", host, cfg.Port) produces an invalid address for any IPv6 host. host: "::1" yields ::1:4000 and host: "::" yields :::4000; net.Listen rejects both with "too many colons in address", so the server fails to start. Since the whole point of the option is picking a bind interface, IPv6 literals are a legitimate value — net.JoinHostPort(host, strconv.Itoa(cfg.Port)) handles the bracketing and also returns :4000 for an empty host.

maglev/cmd/api/app.go

Lines 204 to 206 in 229117a

srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", host, cfg.Port),
Handler: handler,

  1. config.example.json sets "host": "127.0.0.1", which changes the bind behavior of the documented onboarding path. Both README.markdown ("cp config.example.json config.json") and CLAUDE.md ("Copy config.example.json to config.json") tell operators to copy this file verbatim, so every fresh setup would go from all-interfaces to loopback-only — unreachable from a VM host, a LAN device, or a container. It also contradicts the "default": "0.0.0.0" documented one file over in config.schema.json and the PR description's "defaults to 0.0.0.0 to preserve existing behavior". Either omit the key from the example or set it to the documented default.

"port": 4000,
"host": "127.0.0.1",
"env": "development",

  1. The new conditional has no test coverage for the configured-host path. The only test change updates the existing assertion, which still exercises the empty-host branch; nothing anywhere asserts that a non-empty host (from JSON or the -host flag) actually reaches srv.Addr. CONTRIBUTING.md says "Every new branch or condition introduced by a PR needs test coverage."

maglev/cmd/api/app_test.go

Lines 168 to 170 in 229117a

assert.NotNil(t, srv, "Server should not be nil")
assert.Equal(t, "0.0.0.0:8080", srv.Addr, "Server address should match port and address")
assert.NotNil(t, srv.Handler, "Server handler should be set")

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@aaronbrethorst aaronbrethorst left a comment

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.

Thanks for this — a configurable bind address is a genuinely useful option to have, and the plumbing is done thoroughly: JSONConfig, ToAppConfig, the CLI flag, the schema, and the -f mutual-exclusion path are all handled consistently with how port is threaded through. That's the tedious part and you got it right.

There are three things I need changed before this can land, all in the same area: the defaults change behavior rather than preserve it.

1. 0.0.0.0 is not the current default behavior.

Today Addr is fmt.Sprintf(":%d", cfg.Port). Go binds a bare :port as a dual-stack IPv6 wildcard socket ([::]:port with IPV6_V6ONLY=0), which accepts both IPv4 and IPv6 connections. 0.0.0.0:port binds AF_INET only, so IPv6 clients silently stop being able to connect. That's the opposite of the PR's stated goal of preserving existing behavior, and it's the kind of regression that won't show up until someone deploys on an IPv6 network.

The fix is to make the empty host stay empty rather than substituting a literal:

srv := &http.Server{
    Addr: net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)),
    ...
}

net.JoinHostPort("", "4000") yields ":4000", exactly matching today's behavior. Please also update the schema default and the flag help text away from 0.0.0.0 — describing it as "all interfaces" is fine, but 0.0.0.0 specifically is the IPv4-only wildcard.

2. fmt.Sprintf("%s:%d", host, cfg.Port) can't express an IPv6 address.

-host ::1 produces ::1:4000, which net.Listen rejects with "too many colons in address". So the new option works for hostnames and IPv4 literals but not IPv6 ones. net.JoinHostPort (above) handles the bracketing and fixes this at the same time. CodeRabbit flagged this one too.

3. config.example.json shouldn't set "host": "127.0.0.1".

Both README.markdown and CLAUDE.md instruct users to copy config.example.json to config.json verbatim as the setup step. With this line added, every fresh install becomes loopback-only — a new contributor's first make run would work locally but be unreachable from anything else, with nothing obvious pointing at the cause. It also contradicts the schema's own "default": "0.0.0.0" in the same PR.

Please drop the key from config.example.json. The examples block in config.schema.json is a fine place to show "host": "127.0.0.1" as an illustration, since nobody copies that one.

A couple of smaller notes, not blockers:

  • The default would sit more naturally in JSONConfig.setDefaults() (internal/appconf/json_config.go), which is where port, env, rate-limit, data-path, and log-level all resolve their defaults. Putting it in CreateServer means the resolved value never exists at the config layer. This becomes moot if you go with net.JoinHostPort and an empty default, so use your judgment.
  • TestCreateServer only covers the empty-host path. A case asserting that a configured host actually reaches srv.Addr would be worth adding — it's the one branch the feature introduces.
  • The host block in config.schema.json is indented 4 spaces where its siblings use 6.

Happy to re-review as soon as the bind-address defaults are sorted — the rest of this is in good shape.

@sonarqubecloud

sonarqubecloud Bot commented Aug 1, 2026

Copy link
Copy Markdown

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.

Binding Address Configuration

2 participants