halo-dev/halo · warning · ServerWebInputException

urls must not be empty.

Error message

urls must not be empty.

What it means

AttachmentPermalinkMatcher.match(List<String> urls, URL siteUrl) validates its input via createCandidates, which throws ServerWebInputException (HTTP 400) if the urls list is null or empty. The matcher needs at least one candidate URL to do any permalink resolution against stored attachments.

Source

Thrown at application/src/main/java/run/halo/app/core/attachment/AttachmentPermalinkMatcher.java:49

        var candidates = createCandidates(urls, siteUrl);
        var uniqueCandidates = candidates.stream()
                .flatMap(candidate -> candidate.permalinks().stream())
                .collect(Collectors.toCollection(LinkedHashSet::new));
        var listOptions = ListOptions.builder()
                .andQuery(in("status.permalink", uniqueCandidates))
                .build();
        return client.listAll(Attachment.class, listOptions, Sort.unsorted())
                .mapNotNull(AttachmentPermalinkMatcher::getPermalink)
                .collect(Collectors.toSet())
                .map(matchedPermalinks -> candidates.stream()
                        .map(candidate -> new AttachmentPermalinkMatchResult(
                                candidate.url(), candidate.matches(matchedPermalinks)))
                        .toList());
    }

    private static List<Candidate> createCandidates(List<String> urls, URL siteUrl) {
        if (urls == null || urls.isEmpty()) {
            throw new ServerWebInputException("urls must not be empty.");
        }
        return urls.stream().map(url -> createCandidate(url, siteUrl)).toList();
    }

    private static Candidate createCandidate(String url, URL siteUrl) {
        if (!StringUtils.hasText(url)) {
            throw new ServerWebInputException("url must not be blank.");
        }
        var value = url.strip();

        URI valueUri;
        try {
            valueUri = URI.create(value);
        } catch (IllegalArgumentException e) {
            var permalinks = new LinkedHashSet<String>();
            permalinks.add(value);
            return new Candidate(url, permalinks);
        }

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Send at least one URL in the urls field of the request body.
  2. On the client, disable the match action until the user has selected at least one URL.
  3. If invoking match() in code, guard with if (urls == null || urls.isEmpty()) before calling.

Example fix

// before
matcher.match(List.of(), siteUrl) // 400

// after
if (urls != null && !urls.isEmpty()) {
    matcher.match(urls, siteUrl);
}
Defensive patterns

Strategy: validation

Validate before calling

if (urls == null || urls.isEmpty()) {
    return Mono.error(new ServerWebInputException("urls must not be empty."));
}
matcher.match(urls, siteUrl);

Type guard

static boolean hasUrls(List<String> urls) {
    return urls != null && !urls.isEmpty();
}

Prevention

When it happens

Trigger: Calling the attachment permalink match endpoint (or match() directly) with an empty urls array, e.g. POST { "urls": [] } or omitting the field so it deserializes to null.

Common situations: Frontend 'check links' batch action invoked with no selection; a client sending the request body but with an empty urls list due to a mapping bug; programmatic caller passing Collections.emptyList().

Related errors


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