halo-dev/halo · error · IllegalArgumentException
unable to determine kind and namespace from url
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.
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.
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
- 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.
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
- You have no permission to delete this notification.
- Cannot resolve shared dependency
- Cookie contained signature
- Cookie token[1] did not contain a valid number
- Cookie token was not Base64 encoded; value was
AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14).
Data as JSON: /api/errors/9a090fd68b3219c0.
Report an issue: GitHub.
Appendix: 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)