halo-dev/halo · error · IllegalArgumentException

unable to determine kind and namespace from url, %s

Error message

unable to determine kind and namespace from url, %s

What it means

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.

Source

Thrown at application/src/main/java/run/halo/app/security/authorization/RequestInfoFactory.java:126

        if (!grouplessApiPrefixes.contains(requestInfo.apiPrefix)) {
            // one part (APIPrefix) has already been consumed, so this is actually "do we have
            // four parts?"
            if (currentParts.length < 3) {
                // return a non-resource request
                return requestInfo;
            }

            requestInfo.apiGroup = StringUtils.defaultString(currentParts[0]);
            currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);
        }
        requestInfo.isResourceRequest = true;
        requestInfo.apiVersion = currentParts[0];
        currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);
        // handle input of form /{specialVerb}/*
        Set<String> specialVerbs = Set.of("proxy", "watch");
        if (specialVerbs.contains(currentParts[0])) {
            if (currentParts.length < 2) {
                throw new IllegalArgumentException(
                        String.format("unable to determine kind and namespace from url, %s", request.getPath()));
            }
            requestInfo.verb = currentParts[0];
            currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);
        } else {
            requestInfo.verb = switch (request.getMethod().name().toUpperCase()) {
                case "POST" -> "create";
                case "GET", "HEAD" -> "get";
                case "PUT" -> "update";
                case "PATCH" -> "patch";
                case "DELETE" -> "delete";
                default -> "";
            };
        }
        // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative
        // to kind
        Set<String> namespaceSubresources = Set.of("status", "finalize");
        if (Objects.equals(currentParts[0], "namespaces")) {

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Inspect the full path in the error message and add the missing resource segment after the special verb (e.g. /api/v1/watch/{resource}).
  2. Fix the reverse-proxy rewrite rule that is truncating the path.
  3. 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.
  4. Audit the caller building the watch/proxy URL to include the resource path.

Example fix

// before: watch with no resource -> IllegalArgumentException
//   GET /api/v1/watch
// after: include the resource being watched
//   GET /api/v1/watch/posts
Defensive patterns

Strategy: validation

Validate before calling

// Validate the API path before handing it to RequestInfoFactory:
String[] parts = path.replaceAll("^/+|/$", "").split("/");
// after stripping api prefix + (group) + version, if next is proxy/watch, a resource MUST follow
int i = 0;
if (parts.length > i && Set.of("api","apis").contains(parts[i])) i++;
if (parts.length > i && !Set.of("api").contains(parts[i-1]) /*grouped*/) i++;
if (parts.length > i) i++; // version
if (parts.length > i && Set.of("proxy","watch").contains(parts[i]) && parts.length <= i + 1) {
    throw new IllegalArgumentException("Special verb '" + parts[i] + "' requires a resource segment");
}

Try / catch

try {
    RequestInfo info = RequestInfoFactory.INSTANCE.newRequestInfo(request);
} catch (IllegalArgumentException e) {
    log.warn("Rejected malformed API path {}: {}", request.getPath(), e.getMessage());
    return ServerResponse.badRequest().bodyValue(Map.of("message", e.getMessage()));
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/9a090fd68b3219c0. Report an issue: GitHub.