Skip to content

Comments

fix: resolve database hostname to IP before .env template rendering#17

Merged
konradmichalik merged 2 commits intomainfrom
fix/mittwald-dns-resolve-on-create
Feb 18, 2026
Merged

fix: resolve database hostname to IP before .env template rendering#17
konradmichalik merged 2 commits intomainfrom
fix/mittwald-dns-resolve-on-create

Conversation

@konradmichalik
Copy link
Contributor

@konradmichalik konradmichalik commented Feb 18, 2026

Summary

  • Resolve Mittwald database hostname to IP immediately after TCP connectivity check passes
  • Write IP (not hostname) to .env template, eliminating DNS dependency for all subsequent commands
  • Fixes intermittent ERROR 2005 (HY000): Unknown MySQL server host failures during feature:sync

Background

After Mittwald creates a new database, the DNS entry for the host (e.g. mysql-xyz.pg-s-xxx.db.project.host) flaps intermittently for several minutes. Previously, the .env was uploaded with the hostname, and a separate feature:wait_for_database task was supposed to patch it later. This task did not reliably execute, causing db_sync_tool to fail on DNS resolution.

The fix moves hostname-to-IP resolution into MittwaldApi::create(), right after the connectivity check confirms DNS is working. The .env template is then rendered with the stable IP address from the start.

Changes

  • src/Database/Manager/MittwaldApi.php - Add resolveHostnameToIp() method, call it in create() before initDatabaseConfiguration()

Summary by CodeRabbit

  • Bug Fixes
    • Improved database connection stability by resolving hostnames to IP addresses early during configuration (with a fallback to the original hostname if resolution fails). Keeps existing readiness and reachability checks, reducing DNS-related flapping and making subsequent operations more reliable.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Adds hostname-to-IP resolution to MittwaldApi: after database readiness is confirmed, the hostname is resolved to an IP (with fallback) and that value is passed into database configuration initialization via an updated method signature.

Changes

Cohort / File(s) Summary
Hostname Resolution Integration
src/Database/Manager/MittwaldApi.php
Adds resolveHostnameToIp(string $hostname): string helper that resolves hostnames to IPs with fallback; updates initDatabaseConfiguration() signature to (..., string $databaseHost) and passes resolved IP (or original hostname) after readiness checks; retains existing readiness/reachability checks.

Sequence Diagram(s)

sequenceDiagram
    participant MittwaldApi
    participant DNS as DNS Resolver
    participant DB as MySQL Database
    participant Config as Env Config

    MittwaldApi->>DB: waitUntilReady()
    DB-->>MittwaldApi: ready
    MittwaldApi->>DNS: resolveHostnameToIp(hostname)
    DNS-->>MittwaldApi: ipOrFallback
    MittwaldApi->>Config: initDatabaseConfiguration(db, user, ipOrFallback)
    Config-->>MittwaldApi: configured
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 I sniffed the host, I found its trail,
From names to digits down the vale.
If DNS slips, I hop right back—
An IP path to keep on track. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: resolving database hostname to IP before .env template rendering, which directly addresses the DNS flapping issue mentioned in the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/mittwald-dns-resolve-on-create

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

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 (3)
src/Database/Manager/MittwaldApi.php (3)

265-269: Redundant debug message on the exception path.

When run() throws, two debug() calls are emitted: one inside catch with the error message, and then unconditionally the fallback message below the try/catch. The fallback message adds no extra information on the exception path.

♻️ Proposed fix to deduplicate the log
     } catch (\Throwable $e) {
-        debug("Could not resolve {$hostname} to IP: " . $e->getMessage());
+        debug("Could not resolve {$hostname} to IP, using hostname as fallback: " . $e->getMessage());
+        return $hostname;
     }
 
-    debug("Could not resolve {$hostname} to IP, using hostname as fallback.");
-    return $hostname;
+    debug("Could not resolve {$hostname} to IP, using hostname as fallback.");
+    return $hostname;

Or, more concisely, move the fallback return into the catch and keep a single message per path:

     } catch (\Throwable $e) {
         debug("Could not resolve {$hostname} to IP: " . $e->getMessage());
+        debug("Using hostname as fallback.");
+        return $hostname;
     }
 
-    debug("Could not resolve {$hostname} to IP, using hostname as fallback.");
     return $hostname;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Manager/MittwaldApi.php` around lines 265 - 269, The code emits
two debug messages when resolving {$hostname} fails: one inside the catch for
\Throwable $e and another unconditional fallback debug afterwards; remove the
unconditional debug call and instead move the fallback debug (and the fallback
return/usage of $hostname) into the catch block so that the exception path logs
only once; update the catch in the run() (or the method that calls debug() for
hostname resolution) to log the error and then perform the fallback logic using
$hostname.

265-269: Redundant debug() message on the exception path.

When run() throws, two debug() calls are emitted: one inside catch (with the error message) and then the unconditional fallback message below. The second one adds no new information in that path.

♻️ Proposed cleanup
     } catch (\Throwable $e) {
-        debug("Could not resolve {$hostname} to IP: " . $e->getMessage());
+        debug("Could not resolve {$hostname} to IP, using hostname as fallback: " . $e->getMessage());
+        return $hostname;
     }
 
-    debug("Could not resolve {$hostname} to IP, using hostname as fallback.");
     return $hostname;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Manager/MittwaldApi.php` around lines 265 - 269, Redundant debug
logging: remove the unconditional fallback debug call so we don't log the same
message twice when run() throws; specifically, keep the catch block that logs
debug("Could not resolve {$hostname} to IP: " . $e->getMessage()) and delete the
subsequent debug("Could not resolve {$hostname} to IP, using hostname as
fallback.") after the try/catch in MittwaldApi::run (or the surrounding method)
so only the detailed error is emitted on exception and the fallback message is
not duplicated.

255-271: Document the IPv4-only limitation of gethostbyname() in the docblock.

PHP's gethostbyname() is documented as IPv4-only and will not resolve IPv6 records. If Mittwald ever introduces IPv6-only database hosts, this method will silently fall back to the hostname unchanged since the FILTER_VALIDATE_IP check will fail an IPv6 address. While the current behavior gracefully handles this via the fallback path, documenting this limitation prevents future readers from assuming IPv6 coverage.

📝 Suggested docblock addition
 /**
  * Resolves a database hostname to its IP address on the remote server.
  *
+ * Note: uses gethostbyname() which resolves to IPv4 only. IPv6-only hosts
+ * will be returned unchanged (hostname fallback).
+ *
  * This is called immediately after the TCP connectivity check passes, when DNS is
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Manager/MittwaldApi.php` around lines 255 - 271, Add a docblock
to the resolveHostnameToIp method describing that it uses PHP's gethostbyname()
(IPv4-only) so IPv6 records will not be resolved; state the current behavior
(IPv6 will fail FILTER_VALIDATE_IP and the function will fall back to returning
the original hostname) and note this limitation so future readers know it does
not support IPv6-only hosts and may need a different resolver. Reference
resolveHostnameToIp() and gethostbyname() in the docblock text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Database/Manager/MittwaldApi.php`:
- Around line 255-271: Add a docblock to the resolveHostnameToIp method
explaining that it uses PHP's gethostbyname() (invoked via the inline PHP
command in $resolveCmd) which is IPv4-only and will return the original hostname
on failure (so IPv6-only hosts won't be resolved by this method); mention the
current guard ($ip !== $hostname && filter_var($ip, FILTER_VALIDATE_IP)) handles
this by falling back to the hostname and note the limitation and potential need
for a different resolver (e.g., dns_get_record or getaddrinfo) if Mittwald
introduces IPv6-only DB hosts so future maintainers don’t assume IPv6 coverage.

---

Nitpick comments:
In `@src/Database/Manager/MittwaldApi.php`:
- Around line 265-269: The code emits two debug messages when resolving
{$hostname} fails: one inside the catch for \Throwable $e and another
unconditional fallback debug afterwards; remove the unconditional debug call and
instead move the fallback debug (and the fallback return/usage of $hostname)
into the catch block so that the exception path logs only once; update the catch
in the run() (or the method that calls debug() for hostname resolution) to log
the error and then perform the fallback logic using $hostname.
- Around line 265-269: Redundant debug logging: remove the unconditional
fallback debug call so we don't log the same message twice when run() throws;
specifically, keep the catch block that logs debug("Could not resolve
{$hostname} to IP: " . $e->getMessage()) and delete the subsequent debug("Could
not resolve {$hostname} to IP, using hostname as fallback.") after the try/catch
in MittwaldApi::run (or the surrounding method) so only the detailed error is
emitted on exception and the fallback message is not duplicated.
- Around line 255-271: Add a docblock to the resolveHostnameToIp method
describing that it uses PHP's gethostbyname() (IPv4-only) so IPv6 records will
not be resolved; state the current behavior (IPv6 will fail FILTER_VALIDATE_IP
and the function will fall back to returning the original hostname) and note
this limitation so future readers know it does not support IPv6-only hosts and
may need a different resolver. Reference resolveHostnameToIp() and
gethostbyname() in the docblock text.

Comment on lines 255 to 271
private function resolveHostnameToIp(string $hostname): string
{
try {
$resolveCmd = sprintf('echo gethostbyname("%s");', addslashes($hostname));
$ip = trim(run("php -r " . escapeshellarg($resolveCmd)));

if ($ip !== $hostname && filter_var($ip, FILTER_VALIDATE_IP)) {
info("Resolved database host {$hostname} to {$ip}");
return $ip;
}
} catch (\Throwable $e) {
debug("Could not resolve {$hostname} to IP: " . $e->getMessage());
}

debug("Could not resolve {$hostname} to IP, using hostname as fallback.");
return $hostname;
}
Copy link

@coderabbitai coderabbitai bot Feb 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

gethostbyname() is IPv4-only — document the limitation.

The PHP docs confirm gethostbyname() "returns the IPv4 address or a string containing the unmodified hostname on failure" and only accepts IPv4 addresses, not IPv6. If Mittwald ever introduces an IPv6-only database host, the resolution will silently return the original hostname (the $ip !== $hostname + FILTER_VALIDATE_IP gate correctly handles this), but the DNS-flapping workaround won't apply. Worth noting in the docblock so future maintainers don't assume IPv6 coverage.

📝 Suggested docblock addition
 /**
  * Resolves a database hostname to its IP address on the remote server.
  *
+ * Note: uses PHP's gethostbyname() which resolves to IPv4 only.
+ * IPv6-only hosts will fall back to the original hostname silently.
+ *
  * This is called immediately after the TCP connectivity check passes, when DNS is
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Manager/MittwaldApi.php` around lines 255 - 271, Add a docblock
to the resolveHostnameToIp method explaining that it uses PHP's gethostbyname()
(invoked via the inline PHP command in $resolveCmd) which is IPv4-only and will
return the original hostname on failure (so IPv6-only hosts won't be resolved by
this method); mention the current guard ($ip !== $hostname && filter_var($ip,
FILTER_VALIDATE_IP)) handles this by falling back to the hostname and note the
limitation and potential need for a different resolver (e.g., dns_get_record or
getaddrinfo) if Mittwald introduces IPv6-only DB hosts so future maintainers
don’t assume IPv6 coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai Acknowledged. The existing docblock already states "Falls back to the original hostname if resolution fails", which covers this behavior. Mittwald MySQL hosts are IPv4 — adding IPv6 future-proofing notes would be speculative at this point.

Copy link

Choose a reason for hiding this comment

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

@konradmichalik Fair point — the fallback documentation is already there, and if the infrastructure is IPv4 today, explicitly calling out a hypothetical IPv6 scenario adds noise rather than value. Thanks for the context!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/Database/Manager/MittwaldApi.php (1)

257-264: Consider validating the resolved value is a well-formed IP address.

After removing filter_var, the only guard is $ip !== $hostname. If the remote PHP runtime emits any output before the echo result (e.g., a deprecation notice or custom error handler output), trim() won't sanitise it and the mangled string passes the !== $hostname check, silently corrupting database_host.

inet_pton() has been available in core PHP since 5.1.0 and works with both IPv4 and IPv6 addresses without requiring ext-filter, making it a suitable drop-in validator here.

♻️ Suggested fix
-            if ($ip !== $hostname) {
+            if ($ip !== $hostname && inet_pton($ip) !== false) {
                 info("Resolved database host {$hostname} to {$ip}");
                 return $ip;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Manager/MittwaldApi.php` around lines 257 - 264, The resolved IP
from the remote PHP run is not validated and may contain extraneous output;
update the resolution logic in MittwaldApi.php (the block that builds
$resolveCmd, calls run(), and assigns $ip) to verify that $ip is a valid IP
address using inet_pton($ip) !== false before accepting it; if validation fails,
log a warning (using info() or a warning logger) and fall back to returning the
original $hostname instead of assigning the mangled value, so only syntactically
valid IPv4/IPv6 addresses replace $hostname.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Database/Manager/MittwaldApi.php`:
- Around line 246-254: Update the docblock for the hostname-resolution routine
in MittwaldApi.php to state that the shell command ($resolveCmd) uses
gethostbyname(), which performs IPv4-only resolution and will return the
original hostname unchanged on failure; explicitly mention the IPv4-only
limitation and the fallback behavior so future maintainers know IPv6 addresses
are not handled and failures leave the hostname unmodified.

---

Nitpick comments:
In `@src/Database/Manager/MittwaldApi.php`:
- Around line 257-264: The resolved IP from the remote PHP run is not validated
and may contain extraneous output; update the resolution logic in
MittwaldApi.php (the block that builds $resolveCmd, calls run(), and assigns
$ip) to verify that $ip is a valid IP address using inet_pton($ip) !== false
before accepting it; if validation fails, log a warning (using info() or a
warning logger) and fall back to returning the original $hostname instead of
assigning the mangled value, so only syntactically valid IPv4/IPv6 addresses
replace $hostname.

@konradmichalik konradmichalik merged commit 77b2070 into main Feb 18, 2026
2 checks passed
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.

1 participant