can1357/oh-my-pi · error · LegacyDestinationError
upload rejected: ${upstreamError}
Error message
upload rejected: ${upstreamError} What it means
This LegacyDestinationError is thrown by the s-ul.eu legacy uploader when the upstream server accepted the HTTP request but returned a JSON body containing a non-empty 'error' field. The library surfaces the upstream message verbatim inside 'upload rejected: <upstreamError>' so the real cause (auth failure, quota, bad file) is visible. It means the upload failed on the server side, not in transport.
Source
Thrown at packages/coding-agent/src/blob-broker/uploaders-legacy.ts:206
function createSulUploader(config: DestinationRuntimeConfig): BlobUploader {
const destination = "s-ul" as const;
let apiKey: string;
try {
apiKey = requireCredential(config, "apiKey");
} catch (error) {
throw failure(destination, error);
}
return {
destination,
async upload(request) {
try {
const body = multipartFile(request, "file", { wizard: "true", key: apiKey, client: "sharex-native" });
const response = await fetchFor(config)(SUL_UPLOAD_URL, { method: "POST", body });
await expectOk(response, destination);
const data = await jsonObject(destination, response);
const upstreamError = firstString(data, ["error"]);
if (upstreamError) throw new LegacyDestinationError(destination, `upload rejected: ${upstreamError}`);
const protocol = firstString(data, ["protocol"]);
const domain = firstString(data, ["domain"]);
const filename = firstString(data, ["filename"]);
const extension = firstString(data, ["extension"]) ?? "";
if (!protocol || !domain || !filename) {
throw new LegacyDestinationError(destination, "the upload response omitted URL components");
}
const url = httpUrl(destination, `${protocol}${domain}/${filename}${extension}`);
const deleteUrl = new URL(SUL_DELETE_URL);
deleteUrl.searchParams.set("key", apiKey);
deleteUrl.searchParams.set("file", filename);
return publication(destination, request, url, {
delete: { method: "GET", url: deleteUrl.href },
remoteId: filename,
});
} catch (error) {
throw failure(destination, error);
}View on GitHub (pinned to 9690622007)
Solutions
- Read the embedded upstreamError text and fix the stated cause (most often the apiKey credential)
- Verify the apiKey in destination credentials is valid and active on s-ul.eu
- Check the file size against the account's upload quota and retry with a smaller file
- Retry after a delay if the error indicates rate limiting
Example fix
// before: stale key fails silently upstream
const body = multipartFile(request, "file", { wizard: "true", key: oldKey, client: "sharex-native" });
// after: validate the key is present and current before uploading
if (!apiKey || apiKey.length < 32) throw new Error("configure a valid s-ul apiKey");
const body = multipartFile(request, "file", { wizard: "true", key: apiKey, client: "sharex-native" }); Defensive patterns
Strategy: try-catch
Validate before calling
if (typeof apiKey !== "string" || !apiKey.trim()) throw new Error("s-ul apiKey credential is required before upload"); Type guard
function hasUpstreamError(data: unknown): data is { error: string } {
return typeof data === "object" && data !== null && "error" in data && typeof (data as { error: unknown }).error === "string" && (data as { error: string }).error.trim().length > 0;
} Try / catch
try {
await uploader.upload(request);
} catch (err) {
if (err instanceof Error && err.name === "LegacyDestinationError" && /upload rejected:/.test(err.message)) {
const upstream = err.message.split("upload rejected: ")[1];
logger.warn("s-ul rejected upload", { upstream });
}
throw err;
} Prevention
- Keep the s-ul apiKey valid and rotate before expiry
- Check file size against the account quota before uploading
- Treat any 'error' field in a 200 response as fatal and surface it to logs
- Add a periodic key-validity check against the API
When it happens
Trigger: POSTing multipart data to https://s-ul.eu/api/v1/upload succeeds with a 2xx (expectOk passes) but the JSON response includes an 'error' string — e.g. invalid/missing API key, file too large, or rate limiting wrapped in a 200 response.
Common situations: Expired or revoked s-ul API key; uploading a file above the account's size quota; the s-ul service changing its error payload shape while still returning HTTP 200; transient upstream rate limits.
Related errors
- the upload response omitted URL components
- Gemini Files API upload initialization failed with HTTP ${st
- OpenAI Files API upload failed with HTTP ${response.status}
- ${destination} upload failed with HTTP ${status}
- the replacement endpoint rejected the upload
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/5c3b81ce79fe6bb8.
Report an issue: GitHub.