juanfont/headscale · error

not confirmed, aborting

Error message

not confirmed, aborting

What it means

Error (with body and redirectURL returned) from doLoginURLWithClient when followRedirects is true but the final status is not 200: the login flow followed redirects and landed on a non-OK page — meaning login did not complete (error page, 4xx/5xx after redirect, or a 3xx that was followed to something unexpected).

Source

Thrown at hscontrol/api/v1/nodes.go:25

	"net/netip"
	"slices"
	"strconv"
	"time"

	"github.com/danielgtaylor/huma/v2"
	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/juanfont/headscale/hscontrol/util"
	"tailscale.com/net/tsaddr"
	"tailscale.com/tailcfg"
	"tailscale.com/types/key"
)

func init() {
	registrations = append(registrations, registerNodes)
}

// errBackfillNotConfirmed guards BackfillNodeIPs behind explicit confirmed=true.
var errBackfillNotConfirmed = errors.New("not confirmed, aborting")

// registerMethodToV1Enum maps the stored register method onto the
// SCREAMING_SNAKE enum string the v1 contract emits.
var registerMethodToV1Enum = map[string]string{
	util.RegisterMethodAuthKey: "REGISTER_METHOD_AUTH_KEY",
	util.RegisterMethodOIDC:    "REGISTER_METHOD_OIDC",
	util.RegisterMethodCLI:     "REGISTER_METHOD_CLI",
}

// Node mirrors the v1 Node message. The protojson contract emits unpopulated
// fields: scalars and slices always (no omitempty), nested messages and optional
// timestamps as JSON null when unset.
type Node struct {
	ID              string          `format:"uint64"                                                                                      json:"id"`
	MachineKey      string          `json:"machineKey"`
	NodeKey         string          `json:"nodeKey"`
	DiscoKey        string          `json:"discoKey"`
	IPAddresses     []string        `json:"ipAddresses"                                                                                   nullable:"false"`

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Read the logged body (it is log.Printf'd) to see the provider's error message.
  2. Verify OIDC config: client_id, client_secret, redirect URL matching headscale's /oidc/callback.
  3. Check headscale and provider logs for the failing exchange.
  4. Confirm the flow accounts for the confirmation interstitial (see submitConfirmForm) if the deployment renders one.
Defensive patterns

Strategy: try-catch

Try / catch

body, redir, err := doLoginURLWithClient(hostname, loginURL, hc, true)
if err != nil {
    if strings.Contains(err.Error(), "unexpected status code") {
        // body and redir are populated; log them and inspect provider state
        log.Printf("login failed, body=%s redirect=%v", body, redir)
    }
    return err
}

Prevention

When it happens

Trigger: OIDC interactive login where after following redirects the server returns an error page (401/403/500), or the flow ends on a redirect target that is not the success page.

Common situations: OIDC provider rejecting the client (bad client_id/secret), headscale OIDC callback mismatch (redirect URI not registered), expired nonce/state, or the interstitial confirmation step not completed.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/19cece66aa0493b8. Report an issue: GitHub.