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.HostnameView on GitHub (pinned to 2253eeeb25)
Solutions
- Read the formatted API errors in the message for the exact application-level cause
- Retry — many application-level provisioning failures are transient
- Update cloudflared to the latest version in case the API contract changed
- 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
- Keep cloudflared up to date so the QuickTunnelResponse contract stays in sync with the API
- Treat HTTP 200 as insufficient — always check the parsed errors array in API responses
- Retry transient application-level errors with backoff before giving up
- Use named tunnels where provisioning guarantees matter
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
- quick tunnel provisioning failed with status %d: %s
- quick tunnel provisioning failed
- ErrAPINoSuccess
- Failed to fetch page. Server returned: %d
- API errors: %s
AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06).
Data as JSON: /api/errors/19b1959224e557d3.
Report an issue: GitHub.