mihomo-party-org/clash-party · error
Plugin URL must not contain a fragment
Error message
Plugin URL must not contain a fragment
What it means
parseDownloadUrl rejects URLs containing a fragment (#...) because fragments are client-side pointers and make download identity/caching ambiguous. A URL with a # part throws 'Plugin URL must not contain a fragment'.
Source
Thrown at src/main/resolve/plugin/remote.ts:15
import { getAppConfig } from '../../config/app'
import { MAX_PLUGIN_FILE_BYTES } from './constants'
import { requestOnce } from './http-client'
import { createGuardedLookup, isForbiddenHost } from './net-guard'
function parseDownloadUrl(url: string): URL {
let parsed: URL
try {
parsed = new URL(url)
} catch {
throw new Error('Invalid plugin URL')
}
if (parsed.protocol !== 'https:') throw new Error('Plugin URL must use https')
if (parsed.username || parsed.password) throw new Error('Plugin URL must not contain userinfo')
if (parsed.hash) throw new Error('Plugin URL must not contain a fragment')
if (isForbiddenHost(parsed.hostname)) throw new Error('Plugin URL must use a public host')
return parsed
}
export async function fetchRemotePlugin(url: string): Promise<string> {
const parsed = parseDownloadUrl(url)
const { subscriptionTimeout = 30000, pluginUseProxy } = await getAppConfig()
let proxy: { host: string; port: number } | undefined
if (pluginUseProxy) {
const { getControledMihomoConfig } = await import('../../config/controledMihomo')
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
proxy = { host: '127.0.0.1', port }
}
const response = await requestOnce(parsed.toString(), {
method: 'GET',
headers: { Accept: 'application/json, application/octet-stream' },
timeout: subscriptionTimeout,View on GitHub (pinned to 911e090537)
Solutions
- Strip the '#...' portion of the URL before calling.
- If you need a version/cache-buster, use a query string (?v=2) if the server supports it — but prefer the canonical bare file URL.
- Copy the direct file link from the source rather than an anchored page link.
Example fix
// before
await fetchRemotePlugin('https://example.com/plugin.yaml#install')
// after
await fetchRemotePlugin('https://example.com/plugin.yaml') Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(input)
if (u.hash) throw new Error('strip the #fragment from the download URL') Type guard
const hasNoFragment = (s: string): boolean => {
try { return !new URL(s).hash } catch { return false }
} Try / catch
try {
await fetchRemotePlugin(input)
} catch (e) {
if (e.message === 'Plugin URL must not contain a fragment') {
showUrlInputError('Remove the #... part of the URL')
} else throw e
} Prevention
- Strip u.hash before calling when normalizing URLs
- Copy direct file links, not anchored page links
- Use query params, not fragments, for cache-busting
- Validate stored URLs once at config time
When it happens
Trigger: Passing a download URL that ends with or contains '#...' — e.g. a documentation link copied with an anchor (https://example.com/plugin.yaml#install), or a template string where a placeholder got glued to a '#id'.
Common situations: Copying a link from a rendered page that includes a section anchor; appending cache-busting '#v2' instead of a query param; string concatenation bugs leaving a '#fragment' suffix.
Related errors
- Invalid plugin URL
- Unsupported plugin file type
- Plugin path is not a file
- Plugin file too large
- Plugin URL must use https
AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30).
Data as JSON: /api/errors/7776e588048e8ce2.
Report an issue: GitHub.