Kareadita/Kavita · warning · KavitaException

url-malformed

Error message

url-malformed

What it means

Thrown by UrlValidationService.ValidateUrlAsync when Uri.TryCreate(url, UriKind.Absolute, ...) fails — the input is not a parseable absolute URI. ValidateUrlAsync is Kavita's SSRF pre-flight gate invoked before fetching any user-supplied URL (cover images via CoverDbService/UploadController, favicons, Google Fonts, CBL upload and CBL sync). It is a localized KavitaException surfaced as HTTP 500; some callers (UploadController, CBLController) catch it and return 400 instead.

Source

Thrown at Kavita.Services/UrlValidationService.cs:17

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Kavita.API.Services;
using Kavita.Common;
using Kavita.Common.Helpers;

namespace Kavita.Services;

public class UrlValidationService(ILocalizationService localizationService) : IUrlValidationService
{
    public async Task ValidateUrlAsync(string url)
    {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
        {
            throw new KavitaException(await localizationService.TranslateAsync("url-malformed"));
        }

        if (!string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase))
        {
            throw new KavitaException(await localizationService.TranslateAsync("url-https-only"));
        }

        IPAddress[] addresses;
        try
        {
            addresses = await Dns.GetHostAddressesAsync(uri.Host);
        }
        catch (SocketException)
        {
            throw new KavitaException(await localizationService.TranslateAsync("url-unable-to-resolve"));
        }

        if (addresses.Length == 0)

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Trim and encode the URL, then ensure it starts with https:// before submitting.
  2. Validate client-side with `new URL(url)` (JS) / `Uri.IsWellFormedUriString(url, Absolute)` before calling the API.
  3. If only a host is available, prefix 'https://' on the client so the value is absolute.

Example fix

// before
uploadByUrl(url: string) { return this.http.post('upload/upload-by-url', { url }); }
// after
uploadByUrl(raw: string) {
  const url = raw.trim();
  try { if (new URL(url).protocol !== 'https:') throw 0; }
  catch { return throwError(() => new Error('A valid https:// URL is required')); }
  return this.http.post('upload/upload-by-url', { url });
}
Defensive patterns

Strategy: validation

Validate before calling

function isAbsoluteUrl(url: string): boolean {
  try { const u = new URL(url.trim()); return u.protocol === 'http:' || u.protocol === 'https:'; }
  catch { return false; }
}

Type guard

function isAbsoluteHttpsCandidate(s: unknown): s is string {
  return typeof s === 'string' && isAbsoluteUrl(s);
}

Try / catch

try { await svc.fetchFromUrl(url); } catch (e) { if (/malformed/i.test(e.message)) showUser('Enter a valid https:// URL'); else throw e; }

Prevention

When it happens

Trigger: Any code path that calls ValidateUrlAsync with a value that is not an absolute URI: relative paths, strings with unencoded spaces, missing scheme (e.g. 'example.com/foo'), or garbage. Reachable via upload-by-url, CBL upload-cbl-file, cover-from-url, favicon fetch, font download, and CBL URL sync.

Common situations: User pastes a bare domain without https://; a cover URL is copied with a trailing space or newline; a relative '/covers/x.png' is stored where an absolute URL was expected; clipboard copy loses the scheme.

Understand the failure class

Related errors


AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13). Data as JSON: /api/errors/fdedf1d9232338fd. Report an issue: GitHub.