{"record":{"id":"3f9d9bdfc7c090b7","repo":"juanfont/headscale","slug":"mockoidc-client-id-not-defined","errorCode":null,"errorMessage":"MOCKOIDC_CLIENT_ID not defined","messagePattern":"MOCKOIDC_CLIENT_ID not defined","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/headscale/cli/mockoidc.go","lineNumber":21,"sourceCode":"import (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/util/zlog/zf\"\n\t\"github.com/oauth2-proxy/mockoidc\"\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\terrMockOidcClientIDNotDefined     = errors.New(\"MOCKOIDC_CLIENT_ID not defined\")\n\terrMockOidcClientSecretNotDefined = errors.New(\"MOCKOIDC_CLIENT_SECRET not defined\")\n\terrMockOidcPortNotDefined         = errors.New(\"MOCKOIDC_PORT not defined\")\n\terrMockOidcUsersNotDefined        = errors.New(\"MOCKOIDC_USERS not defined\")\n)\n\nconst refreshTTL = 60 * time.Minute\n\nvar accessTTL = 2 * time.Minute\n\nfunc init() {\n\trootCmd.AddCommand(mockOidcCmd)\n}\n\nvar mockOidcCmd = &cobra.Command{\n\tUse:   \"mockoidc\",\n\tShort: \"Runs a mock OIDC server for testing\",\n\tLong:  \"This internal command runs a OpenID Connect for testing purposes\",\n\tRunE: func(cmd *cobra.Command, args []string) error {","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/cmd/headscale/cli/mockoidc.go#L3-L39","documentation":"This is headscale's generic 500 response emitted by the httpError helper whenever a handler returns a Go error that is not an HTTPError (errors.AsType[HTTPError] fails). The real cause is not in the response body by design — it is logged server-side via zerolog with 'http internal server error', the error, and code 500.","triggerScenarios":"Any handler calling httpError(w, err) with a plain error: JSON marshal failures in writeJSON, database/GORM errors surfacing from state or db layers, unexpected nil maps or panics converted to errors in OIDC, registration, or key endpoints.","commonSituations":"Corrupted or locked SQLite database file (disk full, permissions); PostgreSQL unreachable so a DB call returns an error that bubbles into a handler; a node/user record in a state the code did not expect; an object containing channels/funcs failing json.Marshal.","solutions":["Read the server log for the matching line: log.Error().Err(err).Int(\"code\",500).Msg(\"http internal server error\") — the Err field holds the actual cause; fix that root cause.","Check database health: SQLite file permissions/disk space, or PostgreSQL connectivity and migrations being up to date.","If you control the calling code, wrap the failure in an HTTPError (via the HTTPError type) so the client gets a meaningful status/message instead of the generic 500.","Reproduce with headscale serve in the foreground to capture the log line while issuing the failing request."],"exampleFix":"// before: caller gets an opaque 500\nhttpError(w, fmt.Errorf(\"lookup node %q: %w\", name, err))\n\n// after: surface a typed, loggable error\nvar HTTPErrorNotFound = NewHTTPError(http.StatusNotFound, err, fmt.Sprintf(\"node %q not found\", name))\nhttpError(w, HTTPErrorNotFound)","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := client.Do(req)\nif err == nil && resp.StatusCode == http.StatusInternalServerError {\n    body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))\n    // generic 500s here are non-specific: log correlation ID/time and check server zerolog output\n    log.Printf(\"headscale 500 at %s: server log holds the cause (Err field)\", time.Now().UTC())\n    _ = body\n}","preventionTips":["Watch the headscale log whenever you see a bare 'internal server error' — the response intentionally hides the cause.","Monitor DB health (SQLite disk space/permissions, PostgreSQL reachability) to prevent the most common root causes.","In your own handlers, always wrap errors in HTTPError so clients get actionable statuses instead of 500."],"tags":["http","internal-error","logging","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}