cloudflare/cloudflared · error

quick tunnel provisioning failed: %s

Error message

quick tunnel provisioning failed: %s

What it means

After a 2xx response, RunQuickTunnel unmarshals the body; if the parsed QuickTunnelResponse has a non-empty `errors` array, provisioning is treated as failed and the formatted API errors are returned even though HTTP said success. The API reports application-level failures inside an HTTP 200 envelope.

Source

Thrown at cmd/cloudflared/tunnel/quick_tunnel.go:101

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		var data QuickTunnelResponse
		if err := json.Unmarshal(respBody, &data); err == nil && len(data.Errors) > 0 {
			return fmt.Errorf("quick tunnel provisioning failed with status %d: %s", resp.StatusCode, formatQuickTunnelErrors(data.Errors))
		}
		return fmt.Errorf("quick tunnel provisioning failed with status %d: %s", resp.StatusCode, string(respBody))
	}

	var data QuickTunnelResponse
	if err := json.Unmarshal(respBody, &data); err != nil {
		respString := string(respBody)
		fields := map[string]interface{}{"status_code": resp.Status}
		sc.log.Err(err).Fields(fields).Msgf("Error unmarshaling QuickTunnel response: %s", respString)
		return errors.Wrap(err, "failed to unmarshal quick Tunnel")
	}

	// TODO(TUN-10791): Add CLI-level coverage that provisioning errors are logged to users.
	if len(data.Errors) > 0 {
		return fmt.Errorf("quick tunnel provisioning failed: %s", formatQuickTunnelErrors(data.Errors))
	}

	if !data.Success {
		return errors.New("quick tunnel provisioning failed")
	}

	tunnelID, err := uuid.Parse(data.Result.ID)
	if err != nil {
		return errors.Wrap(err, "failed to parse quick Tunnel ID")
	}

	credentials := connection.Credentials{
		AccountTag:   data.Result.AccountTag,
		TunnelSecret: data.Result.Secret,
		TunnelID:     tunnelID,
	}

	url := data.Result.Hostname

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Read the formatted API errors in the message for the exact application-level cause
  2. Retry — many application-level provisioning failures are transient
  3. Update cloudflared to the latest version in case the API contract changed
  4. Fall back to a named tunnel if quick tunnels remain rejected in your environment
Defensive patterns

Strategy: try-catch

Validate before calling

var probe QuickTunnelResponse
if err := json.Unmarshal(respBody, &probe); err == nil && len(probe.Errors) > 0 {
    return fmt.Errorf("API-level errors despite HTTP 200: %v", probe.Errors)
}

Type guard

func hasAPIErrors(r QuickTunnelResponse) bool { return len(r.Errors) > 0 }

Try / catch

tunnel, err := RunQuickTunnel(c)
if err != nil {
    log.Error().Err(err).Msg("quick tunnel rejected; check formatted API errors")
    return err
}

Prevention

When it happens

Trigger: The provisioning API returns HTTP 200 with a JSON body where `success` handling is deferred and `errors` is populated — application-level rejection such as capacity or policy errors.

Common situations: Cloudflare-side capacity/policy rejections delivered as 200+errors; API contract changes between cloudflared versions and the provisioning backend; intermediaries that rewrite the body while keeping the 200 status.

Related errors


AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/19b1959224e557d3. Report an issue: GitHub.