semaphoreui/semaphore · warning

Not Found

Error message

Not Found

What it means

servePublic (api/router.go:612) is the SPA static-file handler: any request whose path equals or falls under <web_path>/api is rejected with 404 'Not Found'. This stops the public-asset fallback from swallowing API routes — if you see it, the API route you intended was not registered or the request bypassed the API router and was served by the static handler.

Solutions

  1. Verify the endpoint path and HTTP method against the API docs / registered routes for your Semaphore version.
  2. Upgrade Semaphore if the endpoint was added in a newer release.
  3. Check reverse-proxy rules preserve the /api prefix and forward to the API router.
  4. Confirm the request is not being redirected (trailing-slash or proxy rewrite) into the public asset handler.

Example fix

// before
fetch("/api/my-new-endpoint")   // 404 via servePublic
// after: use an existing route/version
fetch("/api/ping")  // or upgrade server to a version with the route
Defensive patterns

Strategy: validation

Validate before calling

// guard API calls client-side
const API_BASE = "/api";
function apiPath(p) { return API_BASE + (p.startsWith("/") ? p : "/" + p); }
// use apiPath("/ping") so typos are caught by shared constants + route whitelist

Type guard

function isKnownApiRoute(p, knownRoutes) {
  return knownRoutes.some(r => r.method === p.method && new RegExp(r.pattern).test(p.path));
}

Try / catch

try {
  const res = await fetch(apiPath(endpoint));
  if (res.status === 404) throw new Error("Endpoint missing on this server version");
} catch (e) { /* fall back to supported endpoint or prompt upgrade */ }

Prevention

When it happens

Trigger: Request to a path like /api/... (or /api-something after web_path prefix) that matched the public file server instead of the API router — e.g. wrong HTTP method on a registered route, misspelled API path, or the API subrouter not mounting that endpoint, letting the catch-all servePublic handle it.

Common situations: Typos in API URLs (/api/v2alpha vs /api/v2alpha/); calling a newer endpoint against an older Semaphore version that lacks the route; method mismatch (GET vs POST) on a route registered for another method; reverse proxy stripping or mangling the path prefix.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/0bc51bb7af5265bb. Report an issue: GitHub.

Appendix: source

Thrown at api/router.go:612

	if err != nil {
		fmt.Println(err)
	}
}

func servePublic(w http.ResponseWriter, r *http.Request) {
	webPath := "/"
	if util.WebHostURL != nil {
		webPath = util.WebHostURL.Path
		if !strings.HasSuffix(webPath, "/") {
			webPath += "/"
		}
	}

	reqPath := r.URL.Path
	apiPath := path.Join(webPath, "api")

	if reqPath == apiPath || strings.HasPrefix(reqPath, apiPath) {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	// Check if this is a request for the swagger UI
	swaggerPath := path.Join(webPath, "swagger")
	if reqPath == swaggerPath || reqPath == swaggerPath+"/" {
		serveFile(w, r, "swagger/index.html")
		return
	}

	if !strings.Contains(reqPath, ".") {
		serveFile(w, r, "index.html")
		return
	}

	newPath := strings.Replace(
		reqPath,
		webPath,

View on GitHub (pinned to 1774ccb71a)