{"record":{"id":"a0d90c5d21ad4945","repo":"can1357/oh-my-pi","slug":"seafile-upload-link-response-did-not-include-a-url","errorCode":null,"errorMessage":"seafile upload-link response did not include a URL","messagePattern":"seafile upload-link response did not include a URL","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts","lineNumber":406,"sourceCode":"\tconst token = requireCredential(config, \"authToken\");\n\tconst headers = { Authorization: `Token ${token}` };\n\tconst raw = optionBoolean(config, \"raw\", true) ?? true;\n\tconst expiryDays = optionNumber(config, \"expiryDays\");\n\tconst sharePassword = credentialString(config, \"sharePassword\");\n\tconst requestFetch = fetchFor(config);\n\n\treturn {\n\t\tdestination: \"seafile\",\n\t\tasync upload(request) {\n\t\t\tconst filename = safeFileName(request);\n\t\t\tconst expiryInfo = expiry(expiryDays);\n\t\t\tconst linkResponse = await expectOk(\n\t\t\t\tawait requestFetch(`${endpoint(apiUrl, \"repos\", repositoryId, \"upload-link\")}/?format=json`, { headers }),\n\t\t\t\t\"seafile\",\n\t\t\t);\n\t\t\tconst linkBody = parseJsonText(await linkResponse.text());\n\t\t\tconst fileServerUrl = uploadLink(linkBody);\n\t\t\tif (!fileServerUrl) throw new Error(\"seafile upload-link response did not include a URL\");\n\t\t\tawait expectOk(\n\t\t\t\tawait requestFetch(fileServerUrl, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders,\n\t\t\t\t\tbody: multipartFile(request, \"file\", { filename, parent_dir: directory || \"/\" }),\n\t\t\t\t}),\n\t\t\t\t\"seafile\",\n\t\t\t);\n\n\t\t\tconst shareForm = new URLSearchParams({\n\t\t\t\tp: `${directory === \"/\" ? \"\" : directory}/${filename}`,\n\t\t\t\tshare_type: \"download\",\n\t\t\t});\n\t\t\tif (sharePassword) shareForm.set(\"password\", sharePassword);\n\t\t\tif (expiryDays !== undefined && expiryDays > 0) shareForm.set(\"expire\", String(expiryDays));\n\t\t\tconst shareResponse = await expectOk(\n\t\t\t\tawait requestFetch(`${endpoint(apiUrl, \"repos\", repositoryId, \"file\", \"shared-link\")}/`, {\n\t\t\t\t\tmethod: \"PUT\",","sourceCodeStart":388,"sourceCodeEnd":424,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts#L388-L424","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n{ \"options\": { \"apiUrl\": \"https://seafile.example.com\", ... } }  // web UI base, not API\n// after\n{ \"options\": { \"apiUrl\": \"https://seafile.example.com/seafhttp/api2\" } }  // or /api2 per your deployment","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function hasUploadLink(v) {\n  if (typeof v === 'string' && v.length > 0) return true;\n  if (typeof v !== 'object' || v === null) return false;\n  return ['url', 'upload_link', 'uploadLink'].some(k => typeof v[k] === 'string' && v[k].length > 0);\n}","tryCatchPattern":"try {\n  await uploader.upload(request);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('upload-link response did not include a URL')) {\n    // capture/log the raw response body (parseJsonText fallback text) to see the HTML/error payload,\n    // then fix apiUrl/authToken/proxy before retrying\n  } else throw err;\n}","preventionTips":["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."],"tags":["seafile","api-response","parsing","network"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}