{"record":{"id":"1f3fc237ed25824a","repo":"PaddlePaddle/PaddleOCR","slug":"done-job-response-is-missing-resulturl-jsonurl","errorCode":null,"errorMessage":"Done job response is missing resultUrl.jsonUrl.","messagePattern":"Done job response is missing resultUrl\\.jsonUrl\\.","errorType":"exception","errorClass":"ResponseFormatError","httpStatus":null,"severity":"error","filePath":"api_sdk/typescript/src/internal/poller.ts","lineNumber":145,"sourceCode":"\nfunction normalizeProgress(progress: unknown): Progress | undefined {\n  if (progress === undefined || progress === null) {\n    return undefined;\n  }\n  if (!isRecord(progress)) {\n    throw new ResponseFormatError(\"Status progress must be an object.\");\n  }\n  return {\n    totalPages: typeof progress.totalPages === \"number\" ? progress.totalPages : 0,\n    extractedPages: typeof progress.extractedPages === \"number\" ? progress.extractedPages : 0,\n    startTime: typeof progress.startTime === \"string\" ? progress.startTime : undefined,\n    endTime: typeof progress.endTime === \"string\" ? progress.endTime : undefined,\n  };\n}\n\nfunction resultJsonUrl(data: unknown): string {\n  if (!isRecord(data) || !isRecord(data.resultUrl) || typeof data.resultUrl.jsonUrl !== \"string\") {\n    throw new ResponseFormatError(\"Done job response is missing resultUrl.jsonUrl.\");\n  }\n  return data.resultUrl.jsonUrl;\n}\n\nfunction stringMap(value: Record<string, unknown>): Record<string, string> {\n  const result: Record<string, string> = {};\n  for (const [key, val] of Object.entries(value)) {\n    if (typeof val === \"string\") {\n      result[key] = val;\n    }\n  }\n  return result;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null;\n}\n","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/PaddlePaddle/PaddleOCR/blob/2661c7c0ef5c613e8f93c6e93b2e052399f0f854/api_sdk/typescript/src/internal/poller.ts#L127-L163","documentation":"Thrown by resultJsonUrl() when a job reaches the \"done\" state but its response does not contain a resultUrl object with a string jsonUrl. The poller treats a done job as incomplete without a result location, so it refuses rather than returning a result it cannot fetch. This is a ResponseFormatError pointing at a server-side contract violation or an incomplete job payload.","triggerScenarios":"Poller observes state=\"done\" and immediately tries to read data.resultUrl.jsonUrl, but the server returned done without resultUrl, with resultUrl as a string URL instead of an object, or with jsonUrl missing/non-string. Also occurs when jobs are marked done before the result artifact URL is written.","commonSituations":"Race on the server between status flip to done and result upload; server renames resultUrl.jsonUrl (e.g. to resultUrl.json); resultUrl serialized as a plain string in an older API version.","solutions":["Log the full done-state response body to see what resultUrl actually contains","If resultUrl is a plain string, the server is on an older API shape — align SDK and server versions","If the field is intermittently missing, fix the server to publish the result URL atomically with the done transition","As a caller, retry the status fetch once after a short delay if you control the server and suspect the publish race"],"exampleFix":"// before (server response)\n{ \"state\": \"done\", \"resultUrl\": \"https://cdn/.../result.json\" }\n\n// after\n{ \"state\": \"done\", \"resultUrl\": { \"jsonUrl\": \"https://cdn/.../result.json\" } }","handlingStrategy":"validation","validationCode":"const job = await client.getJob(jobId);\nif (job.state === \"done\" && !(typeof job.resultUrl?.jsonUrl === \"string\")) {\n  // done without a result location: retry once after a delay, then report server bug\n}","typeGuard":"function hasJsonUrl(job: unknown): job is { resultUrl: { jsonUrl: string } } {\n  const j = job as Record<string, any>;\n  return typeof j?.resultUrl?.jsonUrl === \"string\";\n}","tryCatchPattern":"try { await poller.wait(jobId); } catch (e) {\n  if (e instanceof ResponseFormatError && /resultUrl\\.jsonUrl/.test(e.message)) {\n    // likely a publish race on the server: re-fetch status once after ~1s before giving up\n  } else throw e;\n}","preventionTips":["On the server, write the result URL before flipping state to done (atomic completion)","Add integration tests asserting the done payload always contains resultUrl.jsonUrl","Log the full done response when this fires to distinguish rename vs race"],"tags":["api-sdk","typescript","polling","contract-mismatch","race-condition"],"backgroundTag":null,"analyzedSha":"2661c7c0ef5c613e8f93c6e93b2e052399f0f854","analyzedAt":"2026-08-14T20:17:30.180Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}