jackwener/OpenCLI · error · CommandExecutionError
Upwork job-detail store had an unexpected job shape; expecte
Error message
Upwork job-detail store had an unexpected job shape; expected an object.
What it means
The `upwork detail` command reads the job posting from the Vuex store (window.$nuxt.$store.state.jobDetails.job) after the browser-bridge payload reports ready and a job exists. This CommandExecutionError is thrown when that job value is truthy but is not a plain object (e.g. a string, number, or array), meaning Upwork's rehydrated store no longer matches the expected schema. It guards against downstream property access (job.ciphertext, job.title, ...) failing or producing garbage.
Source
Thrown at clis/upwork/detail.js:95
}
catch (e) {
throw new CommandExecutionError(`Failed to read Upwork job-detail store: ${e?.message ?? e}`, 'The Vuex store was not reachable; try again after opening Upwork in the connected browser.');
}
if (payload?.onLogin) {
throw new AuthRequiredError('upwork.com', 'Upwork redirected to login. Open https://www.upwork.com in the connected browser and sign in, then retry.');
}
if (payload?.challenge) {
throw new CommandExecutionError('Upwork served a Cloudflare challenge page', 'Open https://www.upwork.com in the connected browser and clear the challenge, then retry.');
}
if (!isPlainObject(payload)) {
throw new CommandExecutionError('Upwork detail returned an unexpected Browser Bridge payload shape');
}
if (!payload?.ready || !payload.job) {
throw new EmptyResultError('upwork detail', `No Upwork job posting found for id "${id}" (may be closed, expired, or private)`);
}
if (!isPlainObject(payload.job)) {
throw new CommandExecutionError('Upwork job-detail store had an unexpected job shape; expected an object.');
}
const job = payload.job;
const returnedCiphertext = String(job?.ciphertext ?? '').trim();
if (returnedCiphertext && returnedCiphertext !== id) {
throw new CommandExecutionError(`Upwork job-detail store returned ciphertext "${returnedCiphertext}" while reading "${id}".`);
}
const buyer = payload.buyer || {};
const stats = buyer?.stats || {};
const location = buyer?.location || {};
const category = job?.category?.name || '';
const skills = formatSkills(job);
const totalSpent = Number(stats?.totalCharges?.amount);
const totalHires = Number(stats?.totalJobsWithHires);
const score = Number(stats?.score);
const totalApplicants = Number(job?.clientActivity?.totalApplicants);
return [{View on GitHub (pinned to 49907e53dc)
Solutions
- Retry the command — a transient hydration glitch can produce a malformed store value; re-running often repopulates it correctly.
- Open the job URL (https://www.upwork.com/jobs/<id>) in the connected browser and confirm the page renders normally; if not, wait for Upwork to fix or use the web UI.
- Check for an Upwork A/B or redesign (inspect window.$nuxt.$store.state.jobDetails in DevTools) and update the CLI to a version matching the new store schema.
- If the job genuinely fails to load, fall back to `upwork search` / `upwork feed` list data instead of detail.
Example fix
// before
const job = payload.job; // assumed plain object
// after
const job = payload.job && typeof payload.job === 'object' && !Array.isArray(payload.job)
? payload.job
: (() => { throw new CommandExecutionError('unexpected job shape'); })(); Defensive patterns
Strategy: type-guard
Type guard
function isPlainObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
// after the call: if (!isPlainObject(job)) fall back or retry Try / catch
try {
const detail = await upwork.detail(id);
} catch (e) {
if (/unexpected job shape/.test(e.message)) {
// retry once, then degrade to list-row data
return retryOrFallback(id);
}
throw e;
} Prevention
- Keep the CLI updated against Upwork frontend changes to the Vuex jobDetails shape.
- Treat this as a schema-drift signal: log the raw payload when it fires to aid diagnosis.
- Retry once before surfacing to users, since hydration glitches are often transient.
When it happens
Trigger: Running `upwork detail <id>` when payload.job is truthy but isPlainObject(payload.job) returns false — e.g. Upwork changes jobDetails.job to a wrapped/serialized value (a string, an array of versions, or a null-like sentinel that is still truthy) after a site schema change, or the browser bridge mangles the JSON.parse(JSON.stringify(s.job)) round-trip.
Common situations: Upwork shipping a frontend redesign that changes the Vuex jobDetails shape; running a stale version of this CLI against a newer Upwork build; SSR/hydration differences that put a raw string or non-object blob into store.state.jobDetails.job.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- 当前浏览器适配器不支持文件注入
- Mercury appears to have an existing expense review open; ref
- Could not find the Mercury Submit expense or New expense but
- Mercury returned malformed click result
- Mercury returned malformed create expense click result
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/df420903e035bc64.
Report an issue: GitHub.