{"record":{"id":"df420903e035bc64","repo":"jackwener/OpenCLI","slug":"upwork-job-detail-store-had-an-unexpected-job-shap","errorCode":null,"errorMessage":"Upwork job-detail store had an unexpected job shape; expected an object.","messagePattern":"Upwork job-detail store had an unexpected job shape; expected an object\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/upwork/detail.js","lineNumber":95,"sourceCode":"        }\n        catch (e) {\n            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.');\n        }\n\n        if (payload?.onLogin) {\n            throw new AuthRequiredError('upwork.com', 'Upwork redirected to login. Open https://www.upwork.com in the connected browser and sign in, then retry.');\n        }\n        if (payload?.challenge) {\n            throw new CommandExecutionError('Upwork served a Cloudflare challenge page', 'Open https://www.upwork.com in the connected browser and clear the challenge, then retry.');\n        }\n        if (!isPlainObject(payload)) {\n            throw new CommandExecutionError('Upwork detail returned an unexpected Browser Bridge payload shape');\n        }\n        if (!payload?.ready || !payload.job) {\n            throw new EmptyResultError('upwork detail', `No Upwork job posting found for id \"${id}\" (may be closed, expired, or private)`);\n        }\n        if (!isPlainObject(payload.job)) {\n            throw new CommandExecutionError('Upwork job-detail store had an unexpected job shape; expected an object.');\n        }\n\n        const job = payload.job;\n        const returnedCiphertext = String(job?.ciphertext ?? '').trim();\n        if (returnedCiphertext && returnedCiphertext !== id) {\n            throw new CommandExecutionError(`Upwork job-detail store returned ciphertext \"${returnedCiphertext}\" while reading \"${id}\".`);\n        }\n        const buyer = payload.buyer || {};\n        const stats = buyer?.stats || {};\n        const location = buyer?.location || {};\n        const category = job?.category?.name || '';\n        const skills = formatSkills(job);\n        const totalSpent = Number(stats?.totalCharges?.amount);\n        const totalHires = Number(stats?.totalJobsWithHires);\n        const score = Number(stats?.score);\n        const totalApplicants = Number(job?.clientActivity?.totalApplicants);\n\n        return [{","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/upwork/detail.js#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst job = payload.job; // assumed plain object\n// after\nconst job = payload.job && typeof payload.job === 'object' && !Array.isArray(payload.job)\n    ? payload.job\n    : (() => { throw new CommandExecutionError('unexpected job shape'); })();","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isPlainObject(v) {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n// after the call: if (!isPlainObject(job)) fall back or retry","tryCatchPattern":"try {\n  const detail = await upwork.detail(id);\n} catch (e) {\n  if (/unexpected job shape/.test(e.message)) {\n    // retry once, then degrade to list-row data\n    return retryOrFallback(id);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["upwork","schema-validation-failed","browser-automation","command-execution-error"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}