temporalio/temporal · error

when using internal-frontend, publicClient must be empty

Error message

when using internal-frontend, publicClient must be empty

What it means

This error comes from Config validation in temporal's config package. When the service set defines the internal-frontend service, the publicClient block must be fully omitted, because traffic intended for the public frontend is instead routed through the internal frontend; keeping a publicClient config is contradictory. Validation runs as part of config load/validate and aborts startup with this message.

Source

Thrown at common/config/config.go:706

const (
	ForceTLSConfigAuto      = ""
	ForceTLSConfigInternode = "internode"
	ForceTLSConfigFrontend  = "frontend"
)

// Validate validates this config
func (c *Config) Validate() error {
	if err := c.Persistence.Validate(); err != nil {
		return err
	}

	if err := c.Archival.Validate(&c.NamespaceDefaults.Archival); err != nil {
		return err
	}

	_, hasIFE := c.Services[string(primitives.InternalFrontendService)]
	if hasIFE && (c.PublicClient.HostPort != "" || c.PublicClient.ForceTLSConfig != "" || c.PublicClient.HTTPHostPort != "") {
		return fmt.Errorf("when using internal-frontend, publicClient must be empty")
	}

	switch c.PublicClient.ForceTLSConfig {
	case ForceTLSConfigAuto, ForceTLSConfigInternode, ForceTLSConfigFrontend:
	default:
		return fmt.Errorf("invalid value for publicClient.forceTLSConfig: %q", c.PublicClient.ForceTLSConfig)
	}

	return nil
}

// String converts the config object into a string
func (c *Config) String() string {
	var buf bytes.Buffer
	encoder := yaml.NewEncoder(&buf)
	encoder.SetIndent(2)
	_ = encoder.Encode(c)
	maskedYaml, _ := masker.MaskYaml(buf.String(), masker.DefaultYAMLFieldNames)

View on GitHub (pinned to bde624efd1)

Solutions

  1. Remove the entire publicClient block (hostPort, forceTLSConfig, httpHostPort) from the YAML config when internalFrontend is enabled
  2. Remove the corresponding fields from the programmatically constructed common.Config struct
  3. Re-run config load to confirm validation passes

Example fix

// before
services:
  internalFrontend:
publicClient:
  hostPort: "temporal.example.com:7233"
// after
services:
  internalFrontend:
# publicClient removed entirely
Defensive patterns

Strategy: validation

Validate before calling

func hasPublicClientFields(c *config.Config) bool {
	return c.PublicClient.HostPort != "" ||
		c.PublicClient.ForceTLSConfig != "" ||
		c.PublicClient.HTTPHostPort != ""
}
_, hasIFE := c.Services[string(primitives.InternalFrontendService)]
if hasIFE && hasPublicClientFields(c) {
	// fix before calling Validate/LoadConfig: delete publicClient block
}

Prevention

When it happens

Trigger: Calling (Config).Validate (via LoadConfig/load) when c.Services contains key "internalFrontend" (primitives.InternalFrontendService) while c.PublicClient.HostPort != "" OR c.PublicClient.ForceTLSConfig != "" OR c.PublicClient.HTTPPort/HTTPHostPort is set.

Common situations: Operators deploying temporal with the internal-frontend topology (self-hosted clusters fronting via internal frontend) copy a stock config that still has a publicClient section, or add internalFrontend to services without removing the previously used publicClient.HostPort/forceTLSConfig/httpTransport settings.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/bd2b7250551d2fa6. Report an issue: GitHub.