From 1a4f03bbc8e7ea28208866a4c1bebf665cc005fc Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jun 2026 21:37:48 +0200 Subject: [PATCH] feat(api/v2): expose healthcheck as a documented endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /api/v2/health as a Huma operation so it appears in the v2 OpenAPI spec with a clean JSON schema ({"status": "OK"}). It runs the same health.Check() probe as the v1 healthcheck and is public — it opts out of the global bearer auth and is listed in unauthenticatedAPIPaths. --- pkg/routes/api/v2/health.go | 61 +++++++++++++++++++++++++++++++++++++ pkg/routes/routes.go | 3 ++ 2 files changed, 64 insertions(+) create mode 100644 pkg/routes/api/v2/health.go diff --git a/pkg/routes/api/v2/health.go b/pkg/routes/api/v2/health.go new file mode 100644 index 000000000..674dc7b85 --- /dev/null +++ b/pkg/routes/api/v2/health.go @@ -0,0 +1,61 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import ( + "context" + "net/http" + + "code.vikunja.io/api/pkg/health" + + "github.com/danielgtaylor/huma/v2" +) + +type healthBody struct { + Body struct { + Status string `json:"status" doc:"\"OK\" when the service and its dependencies are reachable." example:"OK"` + } +} + +// RegisterHealthRoutes wires the public healthcheck endpoint onto the Huma API. +func RegisterHealthRoutes(api huma.API) { + Register(api, huma.Operation{ + OperationID: "health", + Summary: "Healthcheck", + Description: "Reports whether the service and its dependencies (database) are reachable. Returns 200 with status \"OK\" when healthy, 500 otherwise. Public — no authentication required.", + Method: http.MethodGet, + Path: "/health", + Tags: []string{"service"}, + // Public: opt out of the globally-applied auth. The path is also listed + // in unauthenticatedAPIPaths so the token middleware lets it through. + Security: []map[string][]string{}, + }, healthcheck) +} + +func init() { AddRouteRegistrar(RegisterHealthRoutes) } + +func healthcheck(_ context.Context, _ *struct{}) (*healthBody, error) { + //nolint:contextcheck // health.Check is the shared v1/v2 probe; it takes no context and uses background contexts for its own pings. + if err := health.Check(); err != nil { + // Mirror v1: a failed check is an internal error; the cause is logged, + // not leaked to the client. + return nil, huma.Error500InternalServerError("Internal server error", err) + } + out := &healthBody{} + out.Body.Status = "OK" + return out, nil +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index a01e12be7..0b98918a0 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -368,6 +368,9 @@ var unauthenticatedAPIPaths = map[string]bool{ // Authorization header, not a JWT; mounted only when that token is set. "/api/v2/test/all": true, "/api/v2/test/:table": true, + + // Public infra healthcheck (a Huma op that opts out of the global auth). + "/api/v2/health": true, } // collectRoutesForAPITokens collects all routes for API token permission checking.