paperclipai/paperclip · error · Error
Invalid public base URL
Error message
Invalid public base URL
What it means
Before publishing, the script builds the public current.json URL from PAPERCLIP_PAGE_BASE_URL (default https://pages.paperclip.ing) and validates it must be HTTPS with no embedded credentials, query string, or fragment. This error means the resulting URL failed that validation.
Solutions
- Set PAPERCLIP_PAGE_BASE_URL to a bare https:// hostname URL with no credentials, query, or hash
- Remove any ?... or #... from the base URL value
- Use the default by unsetting PAPERCLIP_PAGE_BASE_URL if publishing to pages.paperclip.ing
- Confirm the URL parses and protocol === 'https:' before running the publish script
Example fix
// before PAPERCLIP_PAGE_BASE_URL=http://pages.paperclip.ing?env=dev // after PAPERCLIP_PAGE_BASE_URL=https://pages.paperclip.ing
Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(`${baseUrl}/${prefix}/current.json`);
if (u.protocol !== "https:" || u.username || u.password || u.search || u.hash) throw new Error("invalid base URL"); Type guard
const isValidPublicBaseUrl = (raw: string) => { try { const u = new URL(raw.replace(/\/+$/, "")); return u.protocol === "https:" && !u.username && !u.password && !u.search && !u.hash; } catch { return false; } }; Try / catch
try { await main(); } catch (e) { if (e.message === "Invalid public base URL") { /* fix PAPERCLIP_PAGE_BASE_URL */ } } Prevention
- Set PAPERCLIP_PAGE_BASE_URL to a bare https:// host with no path credentials, query, or fragment
- Unset the variable to use the default https://pages.paperclip.ing
- Validate the URL in CI before the publish step
- Avoid pasting URLs copied from browsers (they often carry query/hash)
When it happens
Trigger: PAPERCLIP_PAGE_BASE_URL set to an http:// URL, a URL containing user:pass@, or one with ?query or #fragment after trailing slashes are stripped; the joined prefix also produces an invalid URL that URL parsing rejects or flags.
Common situations: Pointing the script at a local http dev mirror of the page host; pasting a base URL with a tracking query string or credentials; typos like 'https:/pages...' that break parsing.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- CreateOS API URL is invalid.
- database-contract-unmet
- local_stdio_missing_secret
- PAPERCLIP_CHAT_WEBHOOK_PUBLIC_URL must be an HTTPS origin…
- PAPERCLIP_OPENCODE_RUNTIME_DIR is required
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/482ebddf427167c6.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/publish-announcements.ts:82
}
files.push({ file: manifestPath, key: `${prefix}/current.json`, contentType: "application/json", cacheControl: "public,max-age=300" });
return { manifest, files };
}
export function announcementUploadArgs(bucket: string, file: Awaited<ReturnType<typeof prepareAnnouncementPublish>>["files"][number]) {
return ["s3api", "put-object", "--bucket", bucket, "--key", file.key, "--body", file.file,
"--content-type", file.contentType, "--cache-control", file.cacheControl];
}
async function main() {
const { sourceDirectory, staging, publish } = parseAnnouncementPublishArgs(process.argv.slice(2));
const hostPrefix = process.env.PAPERCLIP_PAGE_DEFAULT_PREFIX;
const prepared = await prepareAnnouncementPublish(sourceDirectory, staging, hostPrefix);
const bucket = process.env.PAPERCLIP_PAGE_BUCKET;
const baseUrl = process.env.PAPERCLIP_PAGE_BASE_URL?.replace(/\/+$/, "") ?? "https://pages.paperclip.ing";
const url = `${baseUrl}/${announcementPublishPrefix(staging, hostPrefix)}/current.json`;
const parsed = new URL(url);
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error("Invalid public base URL");
console.log(JSON.stringify({ mode: publish ? "publish" : "dry-run", target: staging ? `staging/${staging}` : "production", bucket: bucket ?? "(unset)", url, announcementId: prepared.manifest.announcement?.id ?? null, files: prepared.files }, null, 2));
if (!publish) return;
if (!bucket) throw new Error("Set PAPERCLIP_PAGE_BUCKET before publishing");
const env = { ...process.env };
const key = env.PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID;
const secret = env.PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY;
if (Boolean(key) !== Boolean(secret)) throw new Error("Set both namespaced page uploader credential variables");
if (key && secret) {
env.AWS_ACCESS_KEY_ID = key;
env.AWS_SECRET_ACCESS_KEY = secret;
delete env.AWS_SESSION_TOKEN;
if (env.PAPERCLIP_PAGE_AWS_SESSION_TOKEN) env.AWS_SESSION_TOKEN = env.PAPERCLIP_PAGE_AWS_SESSION_TOKEN;
} else if (env.PAPERCLIP_PAGE_AWS_PROFILE) {
delete env.AWS_ACCESS_KEY_ID;
delete env.AWS_SECRET_ACCESS_KEY;
delete env.AWS_SESSION_TOKEN;
env.AWS_PROFILE = env.PAPERCLIP_PAGE_AWS_PROFILE;
}View on GitHub (pinned to 3f1d897a7c)