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
- 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.
- Confirm options.repositoryId is a valid library ID your token can access (test GET {apiUrl}/repos/{repositoryId}/ with the token).
- 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.
- Check for a reverse proxy intercepting the request (auth wall, WAF) and whitelist/exclude the API path.
- 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
- Validate the Seafile token and repository access with a cheap GET (/repos/{id}/) before uploads.
- Point apiUrl at the correct API base for your deployment (e.g. /api2 or /seafhttp/api2), not the web UI.
- Ensure reverse proxies/WAFs pass the API endpoints through without redirecting to a login page.
- Log raw API bodies on failure — the fallback text reveals HTML error pages immediately.
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
- Gemini Files API ${context} response is not valid JSON
- ${argv[0]} did not report a tunnel URL within ${READY_TIMEOU
- Gemini Files API finalize response is missing ${field}
- Pushbullet upload fields were missing from the destination r
- Discord returned an invalid message response
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/a0d90c5d21ad4945.
Report an issue: GitHub.