{"record":{"id":"9a090fd68b3219c0","repo":"halo-dev/halo","slug":"unable-to-determine-kind-and-namespace-from-url","errorCode":null,"errorMessage":"unable to determine kind and namespace from url, %s","messagePattern":"unable to determine kind and namespace from url, (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"application/src/main/java/run/halo/app/security/authorization/RequestInfoFactory.java","lineNumber":126,"sourceCode":"        if (!grouplessApiPrefixes.contains(requestInfo.apiPrefix)) {\n            // one part (APIPrefix) has already been consumed, so this is actually \"do we have\n            // four parts?\"\n            if (currentParts.length < 3) {\n                // return a non-resource request\n                return requestInfo;\n            }\n\n            requestInfo.apiGroup = StringUtils.defaultString(currentParts[0]);\n            currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);\n        }\n        requestInfo.isResourceRequest = true;\n        requestInfo.apiVersion = currentParts[0];\n        currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);\n        // handle input of form /{specialVerb}/*\n        Set<String> specialVerbs = Set.of(\"proxy\", \"watch\");\n        if (specialVerbs.contains(currentParts[0])) {\n            if (currentParts.length < 2) {\n                throw new IllegalArgumentException(\n                        String.format(\"unable to determine kind and namespace from url, %s\", request.getPath()));\n            }\n            requestInfo.verb = currentParts[0];\n            currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);\n        } else {\n            requestInfo.verb = switch (request.getMethod().name().toUpperCase()) {\n                case \"POST\" -> \"create\";\n                case \"GET\", \"HEAD\" -> \"get\";\n                case \"PUT\" -> \"update\";\n                case \"PATCH\" -> \"patch\";\n                case \"DELETE\" -> \"delete\";\n                default -> \"\";\n            };\n        }\n        // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative\n        // to kind\n        Set<String> namespaceSubresources = Set.of(\"status\", \"finalize\");\n        if (Objects.equals(currentParts[0], \"namespaces\")) {","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/halo-dev/halo/blob/d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8/application/src/main/java/run/halo/app/security/authorization/RequestInfoFactory.java#L108-L144","documentation":"RequestInfoFactory.newRequestInfo parses Halo's Kubernetes-style API paths for the authorization layer. After consuming the API prefix and the API version segment, if the next segment is a 'special verb' ('proxy' or 'watch') but fewer than 2 segments remain (i.e. the verb has no resource after it), the URL is ambiguous and an IllegalArgumentException is thrown. The error message includes the full request path.","triggerScenarios":"An API request whose path resolves to '/api/{version}/watch' or '/api/{version}/proxy' (or the grouped '/apis/{group}/{version}/watch') with NO resource segment following the special verb, e.g. GET /api/v1/watch. Triggered inside the authorization dispatcher when classifying an incoming API request.","commonSituations":"A misconfigured reverse-proxy rewrite that drops the trailing path segments; a buggy custom route/extension issuing watch/proxy requests; a client SDK constructing a watch URL without a resource. This is an internal routing/programming defect rather than something end-users hit through the UI.","solutions":["Inspect the full path in the error message and add the missing resource segment after the special verb (e.g. /api/v1/watch/{resource}).","Fix the reverse-proxy rewrite rule that is truncating the path.","If the request is genuinely non-resource, route it outside the /api or /apis prefix so RequestInfoFactory returns a non-resource request instead of throwing.","Audit the caller building the watch/proxy URL to include the resource path."],"exampleFix":"// before: watch with no resource -> IllegalArgumentException\n//   GET /api/v1/watch\n// after: include the resource being watched\n//   GET /api/v1/watch/posts","handlingStrategy":"validation","validationCode":"// Validate the API path before handing it to RequestInfoFactory:\nString[] parts = path.replaceAll(\"^/+|/$\", \"\").split(\"/\");\n// after stripping api prefix + (group) + version, if next is proxy/watch, a resource MUST follow\nint i = 0;\nif (parts.length > i && Set.of(\"api\",\"apis\").contains(parts[i])) i++;\nif (parts.length > i && !Set.of(\"api\").contains(parts[i-1]) /*grouped*/) i++;\nif (parts.length > i) i++; // version\nif (parts.length > i && Set.of(\"proxy\",\"watch\").contains(parts[i]) && parts.length <= i + 1) {\n    throw new IllegalArgumentException(\"Special verb '\" + parts[i] + \"' requires a resource segment\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    RequestInfo info = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n} catch (IllegalArgumentException e) {\n    log.warn(\"Rejected malformed API path {}: {}\", request.getPath(), e.getMessage());\n    return ServerResponse.badRequest().bodyValue(Map.of(\"message\", e.getMessage()));\n}","preventionTips":["Always include a resource segment after watch/proxy verbs in API URLs.","Unit-test reverse-proxy rewrite rules against known API paths.","Keep non-resource endpoints outside the /api and /apis prefixes.","Add integration tests for the authorization dispatcher covering special-verb paths."],"tags":["security","authorization","api","routing","requestinfo"],"backgroundTag":null,"analyzedSha":"d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8","analyzedAt":"2026-08-14T00:18:38.915Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}