{"record":{"id":"693d18f42ea24c53","repo":"Freika/dawarich","slug":"translate-error-keys-body-error-poster-order","errorCode":null,"errorMessage":"translate(ERROR_KEYS[body.error] || \"poster.order_errors.generic\")","messagePattern":"translate\\(ERROR_KEYS\\[body\\.error\\] \\|\\| \"poster\\.order_errors\\.generic\"\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/javascript/poster_studio/ui/order_client.js","lineNumber":30,"sourceCode":"export async function submitPrintOrder({\n  url,\n  blob,\n  sku,\n  title,\n  themeBase,\n  layoutId,\n  onProgress,\n}) {\n  const form = new FormData()\n  form.append(\"file\", blob, \"poster.pdf\")\n  form.append(\"sku\", sku)\n  form.append(\"title\", title || \"\")\n  form.append(\"theme_base\", themeBase || \"\")\n  form.append(\"layout_id\", layoutId)\n\n  const { status, body } = await postForm(url, form, onProgress)\n  if (status < 200 || status >= 300) {\n    throw new Error(\n      translate(ERROR_KEYS[body.error] || \"poster.order_errors.generic\"),\n    )\n  }\n  return { token: body.token, checkoutUrl: body.checkout_url }\n}\n\n// XMLHttpRequest instead of fetch solely for upload progress events —\n// print PDFs run tens of MB and fetch has no upload progress API.\nfunction postForm(url, form, onProgress) {\n  return new Promise((resolve, reject) => {\n    const xhr = new XMLHttpRequest()\n    xhr.open(\"POST\", url)\n    xhr.responseType = \"json\"\n    if (onProgress) {\n      xhr.upload.addEventListener(\"progress\", (event) => {\n        if (event.lengthComputable) onProgress(event.loaded / event.total)\n      })\n    }","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/javascript/poster_studio/ui/order_client.js#L12-L48","documentation":"submitPrintOrder uploads the rendered poster PDF via XMLHttpRequest (XHR is used deliberately because fetch has no upload-progress events) and throws a translated, user-facing message when the response status is outside 200-299. The server is expected to return a JSON body with an error code from the known set - wrong_size, too_large, unknown_sku, payment_unavailable, not_pdf, unreadable - mapped through ERROR_KEYS to i18n keys; any other or missing code falls back to poster.order_errors.generic. If the error body is not JSON (e.g. an HTML 502), body.error access can itself throw.","triggerScenarios":"POST of the print-order form where the server rejects the PDF: dimensions not matching the SKU (wrong_size), file above the server's size cap (too_large), a SKU that is not configured for printing (unknown_sku), payment provider unreachable (payment_unavailable), blob not a valid PDF (not_pdf), or a corrupt/unreadable PDF (unreadable). A 500 from an unknown cause yields the generic message.","commonSituations":"Large high-DPI posters exceeding an nginx client_max_body_size or app limit, SKU renamed on the print provider while the UI still offers the old one, checkout backend down or mis-credentialed, generating the blob from a canvas that produced an empty/invalid PDF, proxy timeouts on multi-MB uploads over slow links.","solutions":["Check response body's error code first - it names the exact rejection reason; only trust generic when the code is absent","For too_large: lower the poster DPI/size or raise the server's upload limit (nginx client_max_body_size / Rack config)","For unknown_sku: verify the SKU list sent by the UI matches what the print controller accepts","For not_pdf/unreadable: re-render the blob and confirm it starts with %PDF- before uploading","Guard body being null/non-JSON so an HTML error page does not crash the handler with a TypeError"],"exampleFix":"// before\nconst { status, body } = await postForm(url, form, onProgress)\nif (status < 200 || status >= 300) {\n  throw new Error(translate(ERROR_KEYS[body.error] || 'poster.order_errors.generic'))\n}\n\n// after\nconst { status, body } = await postForm(url, form, onProgress)\nif (status < 200 || status >= 300) {\n  const code = body && typeof body.error === 'string' ? body.error : null\n  throw new Error(translate(ERROR_KEYS[code] || 'poster.order_errors.generic'))\n}","handlingStrategy":"validation","validationCode":"function validateOrderInput({ blob, sku, maxBytes }) {\n  if (!(blob instanceof Blob)) throw new Error('A PDF blob is required')\n  if (blob.size > maxBytes) throw new Error(translate('poster.order_errors.too_large'))\n  if (blob.type && blob.type !== 'application/pdf') throw new Error(translate('poster.order_errors.not_pdf'))\n  if (!sku) throw new Error(translate('poster.order_errors.unknown_sku'))\n}","typeGuard":"/** Narrow a postForm result to a usable body before reading error codes. */\nfunction hasErrorBody(result) {\n  return result.body !== null && typeof result.body === 'object' && 'error' in result.body\n}","tryCatchPattern":"try {\n  return await submitPrintOrder({ url, blob, sku, title, themeBase, layoutId, onProgress })\n} catch (error) {\n  showOrderError(error.message) // already a translated, user-facing string\n  throw error\n}","preventionTips":["Check blob.size against the server's upload cap before starting a multi-MB XHR upload","Verify the blob starts with %PDF- (arrayBuffer peek) before submitting","Validate sku/layout pairs against the same list the server accepts","Keep ERROR_KEYS in sync when the print controller adds new error codes"],"tags":["xhr","upload","poster-studio","i18n","print-order"],"backgroundTag":"file-upload-rejected","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}