#1523·litestream

replica_url: provider detection fails when the endpoint includes an explicit port

Author: corylanouCreated Sep 14, 2026Updated Sep 14, 2026
Labelsbug

Summary

Every Is*Endpoint provider check fails when the endpoint includes an explicit port. extractEndpointHost returns url.URL.Host, which keeps the port, and the matchers then compare that against a bare hostname.

Providers fall back to generic S3 behavior, silently, for a configuration that is otherwise valid.

Reproduction

go
litestream.IsHetznerEndpoint("https://fsn1.your-objectstorage.com")      // true
litestream.IsHetznerEndpoint("https://fsn1.your-objectstorage.com:443")  // false

Run against main:

Provider no port explicit :443
Hetzner true false
Tigris true false
DigitalOcean true false
Backblaze true false
GCS true true

GCS is unaffected because IsGoogleCloudStorageEndpoint does its own parsing rather than relying on the shared suffix match.

Cause

replica_url.go:

go
func extractEndpointHost(endpoint string) string {
	...
	if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") {
		if u, err := url.Parse(endpoint); err == nil && u.Host != "" {
			return u.Host          // includes the port
		}
	}
	return endpoint
}

Callers then do exact or suffix matches against a hostname:

go
return strings.HasSuffix(host, ".your-objectstorage.com")
return host == "fly.storage.tigris.dev" || host == "t3.storage.dev"

u.Host is fsn1.your-objectstorage.com:443; u.Hostname() is fsn1.your-objectstorage.com.

Impact

Today this costs provider-specific defaults: Tigris consistency headers, upload tuning, the GCS accept-encoding workaround, and similar. Quiet and recoverable.

It gets sharper with #1509, which routes Hetzner lease behavior — ETag quoting and conditional DELETE — through IsHetznerEndpoint. A Hetzner user who writes an explicit port would take the generic path that PR exists to avoid, on a code path whose job is mutual exclusion between writers.

This bug predates #1509. Filing separately so that PR is not blocked on it.

Fix

Use u.Hostname() in extractEndpointHost, which strips the port, and add table tests covering explicit ports for each provider matcher. Worth checking whether any caller depends on the port being present before changing it.