halo-dev/halo · warning · ServerWebInputException

url must not be blank.

Error message

url must not be blank.

What it means

AttachmentPermalinkMatcher.createCandidate throws ServerWebInputException (HTTP 400) when an individual entry in the urls list is blank (StringUtils.hasText false). Each candidate is validated independently before URI parsing, so a single blank string fails the whole batch.

Source

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

        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);
        }
        if (isUnsupportedAbsoluteUri(valueUri)) {
            throw new ServerWebInputException("Unsupported URL protocol: " + value);
        }

        var permalinks = new LinkedHashSet<String>();
        permalinks.add(value);
        var siteUri = URI.create(siteUrl.toString());

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Filter blank entries out of the urls list before sending: urls.filter(u => u.trim().length > 0).
  2. Trim and de-duplicate the list client-side.
  3. If using comma-split input, reject/ignore empty tokens after splitting.

Example fix

// before
const urls = raw.split(',') // ["a", "", "b"]

// after
const urls = raw.split(',').map(s => s.trim()).filter(Boolean)
Defensive patterns

Strategy: validation

Validate before calling

List<String> clean = urls.stream()
    .filter(StringUtils::hasText)
    .map(String::strip)
    .distinct()
    .toList();
if (clean.isEmpty()) { /* handle */ }
matcher.match(clean, siteUrl);

Type guard

static boolean noBlankUrls(List<String> urls) {
    return urls != null && urls.stream().allMatch(StringUtils::hasText);
}

Prevention

When it happens

Trigger: Sending a urls list that contains an empty string or a whitespace-only element, e.g. ["https://a.com", "", "https://b.com"], or trailing commas in a comma-separated input that split into blanks.

Common situations: Frontend builds the list from a textarea or multi-select and leaves a blank entry; client splits a comma-joined string and doesn't filter empties; copy-paste introducing stray whitespace.

Related errors


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