can1357/oh-my-pi · error · Error

seafile upload-link response did not include a URL

Error message

seafile upload-link response did not include a URL

What it means

Thrown by the seafile uploader when the response from the repos/{id}/upload-link API neither is a JSON object with a url/upload_link/uploadLink field nor a bare non-empty URL string. parseJsonText falls back to returning raw text, and uploadLink() extracts nothing usable, so the upload-link endpoint's payload could not be interpreted.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts:406

	const token = requireCredential(config, "authToken");
	const headers = { Authorization: `Token ${token}` };
	const raw = optionBoolean(config, "raw", true) ?? true;
	const expiryDays = optionNumber(config, "expiryDays");
	const sharePassword = credentialString(config, "sharePassword");
	const requestFetch = fetchFor(config);

	return {
		destination: "seafile",
		async upload(request) {
			const filename = safeFileName(request);
			const expiryInfo = expiry(expiryDays);
			const linkResponse = await expectOk(
				await requestFetch(`${endpoint(apiUrl, "repos", repositoryId, "upload-link")}/?format=json`, { headers }),
				"seafile",
			);
			const linkBody = parseJsonText(await linkResponse.text());
			const fileServerUrl = uploadLink(linkBody);
			if (!fileServerUrl) throw new Error("seafile upload-link response did not include a URL");
			await expectOk(
				await requestFetch(fileServerUrl, {
					method: "POST",
					headers,
					body: multipartFile(request, "file", { filename, parent_dir: directory || "/" }),
				}),
				"seafile",
			);

			const shareForm = new URLSearchParams({
				p: `${directory === "/" ? "" : directory}/${filename}`,
				share_type: "download",
			});
			if (sharePassword) shareForm.set("password", sharePassword);
			if (expiryDays !== undefined && expiryDays > 0) shareForm.set("expire", String(expiryDays));
			const shareResponse = await expectOk(
				await requestFetch(`${endpoint(apiUrl, "repos", repositoryId, "file", "shared-link")}/`, {
					method: "PUT",

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify options.apiUrl points at the Seafile API base ending in /api2 (or the server's documented API root) and credentials.authToken is a valid, unexpired Seafile token.
  2. Confirm options.repositoryId is a valid library ID your token can access (test GET {apiUrl}/repos/{repositoryId}/ with the token).
  3. Log or print the raw response body (linkBody) — parseJsonText returns trimmed raw text when JSON parsing fails, which reveals an HTML error page or proxy login redirect.
  4. Check for a reverse proxy intercepting the request (auth wall, WAF) and whitelist/exclude the API path.
  5. If running Seafile behind a different base for uploads, ensure the file server URL it returns is reachable and not rewritten to HTML.

Example fix

// before
{ "options": { "apiUrl": "https://seafile.example.com", ... } }  // web UI base, not API
// after
{ "options": { "apiUrl": "https://seafile.example.com/seafhttp/api2" } }  // or /api2 per your deployment
Defensive patterns

Strategy: type-guard

Type guard

function hasUploadLink(v) {
  if (typeof v === 'string' && v.length > 0) return true;
  if (typeof v !== 'object' || v === null) return false;
  return ['url', 'upload_link', 'uploadLink'].some(k => typeof v[k] === 'string' && v[k].length > 0);
}

Try / catch

try {
  await uploader.upload(request);
} catch (err) {
  if (err instanceof Error && err.message.includes('upload-link response did not include a URL')) {
    // capture/log the raw response body (parseJsonText fallback text) to see the HTML/error payload,
    // then fix apiUrl/authToken/proxy before retrying
  } else throw err;
}

Prevention

When it happens

Trigger: Seafile (or a proxy) returning an HTML login/error page, an empty body, a JSON error object without any link field (e.g. {"error_msg": "..."} on bad token), or a differently-shaped API version response.

Common situations: Expired/invalid Seafile auth token returning 200 with an error page via reverse proxy; wrong apiUrl (pointing at the web UI instead of the /seafhttp or /api2 base); API version mismatch (older Seafile naming); SSO/proxy stripping the JSON response.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a0d90c5d21ad4945. Report an issue: GitHub.