Freika/dawarich · error · Error

translate(ERROR_KEYS[body.error] || "poster.order_errors.gen

Error message

translate(ERROR_KEYS[body.error] || "poster.order_errors.generic")

What it means

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.

Source

Thrown at app/javascript/poster_studio/ui/order_client.js:30

export async function submitPrintOrder({
  url,
  blob,
  sku,
  title,
  themeBase,
  layoutId,
  onProgress,
}) {
  const form = new FormData()
  form.append("file", blob, "poster.pdf")
  form.append("sku", sku)
  form.append("title", title || "")
  form.append("theme_base", themeBase || "")
  form.append("layout_id", layoutId)

  const { status, body } = await postForm(url, form, onProgress)
  if (status < 200 || status >= 300) {
    throw new Error(
      translate(ERROR_KEYS[body.error] || "poster.order_errors.generic"),
    )
  }
  return { token: body.token, checkoutUrl: body.checkout_url }
}

// XMLHttpRequest instead of fetch solely for upload progress events —
// print PDFs run tens of MB and fetch has no upload progress API.
function postForm(url, form, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open("POST", url)
    xhr.responseType = "json"
    if (onProgress) {
      xhr.upload.addEventListener("progress", (event) => {
        if (event.lengthComputable) onProgress(event.loaded / event.total)
      })
    }

View on GitHub (pinned to 97fad417c5)

Solutions

  1. Check response body's error code first - it names the exact rejection reason; only trust generic when the code is absent
  2. For too_large: lower the poster DPI/size or raise the server's upload limit (nginx client_max_body_size / Rack config)
  3. For unknown_sku: verify the SKU list sent by the UI matches what the print controller accepts
  4. For not_pdf/unreadable: re-render the blob and confirm it starts with %PDF- before uploading
  5. Guard body being null/non-JSON so an HTML error page does not crash the handler with a TypeError

Example fix

// before
const { status, body } = await postForm(url, form, onProgress)
if (status < 200 || status >= 300) {
  throw new Error(translate(ERROR_KEYS[body.error] || 'poster.order_errors.generic'))
}

// after
const { status, body } = await postForm(url, form, onProgress)
if (status < 200 || status >= 300) {
  const code = body && typeof body.error === 'string' ? body.error : null
  throw new Error(translate(ERROR_KEYS[code] || 'poster.order_errors.generic'))
}
Defensive patterns

Strategy: validation

Validate before calling

function validateOrderInput({ blob, sku, maxBytes }) {
  if (!(blob instanceof Blob)) throw new Error('A PDF blob is required')
  if (blob.size > maxBytes) throw new Error(translate('poster.order_errors.too_large'))
  if (blob.type && blob.type !== 'application/pdf') throw new Error(translate('poster.order_errors.not_pdf'))
  if (!sku) throw new Error(translate('poster.order_errors.unknown_sku'))
}

Type guard

/** Narrow a postForm result to a usable body before reading error codes. */
function hasErrorBody(result) {
  return result.body !== null && typeof result.body === 'object' && 'error' in result.body
}

Try / catch

try {
  return await submitPrintOrder({ url, blob, sku, title, themeBase, layoutId, onProgress })
} catch (error) {
  showOrderError(error.message) // already a translated, user-facing string
  throw error
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21). Data as JSON: /api/errors/693d18f42ea24c53. Report an issue: GitHub.