{"record":{"id":"48d8ece6a2f7c87c","repo":"antiwork/gumroad","slug":"responsedata-error-48d8ec","errorCode":null,"errorMessage":"${responseData.error}","messagePattern":"\\$\\{responseData\\.error\\}","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/thumbnails.ts","lineNumber":24,"sourceCode":"\nexport type ThumbnailPayload = { type: \"file\"; signedBlobId: string };\n\nexport const createThumbnail = async (permalink: string, thumbnailPayload: ThumbnailPayload): Promise<Thumbnail> => {\n  const response = await request({\n    method: \"POST\",\n    accept: \"json\",\n    url: Routes.link_thumbnails_path(permalink),\n    data: {\n      thumbnail: { signed_blob_id: thumbnailPayload.signedBlobId },\n    },\n  });\n\n  if (response.ok) {\n    const responseData = typia.assert<{ success: true; thumbnail: Thumbnail } | { success: false; error: string }>(\n      await response.json(),\n    );\n    if (responseData.success) return responseData.thumbnail;\n    throw new ResponseError(responseData.error);\n  }\n\n  throw new ResponseError();\n};\n\nexport const deleteThumbnail = async (permalink: string, thumbnailId: string) => {\n  const response = await request({\n    method: \"DELETE\",\n    accept: \"json\",\n    url: Routes.link_thumbnail_path(permalink, thumbnailId),\n  });\n\n  if (response.ok) {\n    const responseData = typia.assert<{ success: true; thumbnail: Thumbnail } | { success: false }>(\n      await response.json(),\n    );\n    if (responseData.success) return;\n  }","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/thumbnails.ts#L6-L42","documentation":"createThumbnail (app/javascript/data/thumbnails.ts:9-28) POSTs a signed Active Storage blob id to Routes.link_thumbnails_path and throws ResponseError(responseData.error) when the server answers a parsed body with success:false. This is an application-level rejection: the HTTP exchange completed, but the Rails controller refused to attach the thumbnail and returned its reason in the error string — typically an invalid/expired signed_blob_id, a rejected content type, or a product the seller cannot edit.","triggerScenarios":"Uploading a product thumbnail in ProductEdit/ProductTab/ThumbnailEditor.tsx:62 when the direct-upload blob never actually uploaded (signedBlobId references nothing); the signed id expired before the POST; the file type/size is rejected server-side; the permalink belongs to a product not owned by the acting seller.","commonSituations":"Direct upload to Active Storage silently failing (network drop between S3 upload and attach step); reusing a signed_blob_id captured in an old form state after the upload was redone; tests posting fixture signed ids that were never generated; product permissions changed while the edit page was open.","solutions":["Retry the full upload flow — re-run the direct upload so signedBlobId is freshly minted, then POST again","Verify the direct upload succeeded before calling createThumbnail (check the upload step's own result, not just form state)","Confirm the file is an accepted image type and under the size limit the controller enforces","Log/inspect responseData.error — the server states the exact refusal (invalid blob, wrong type, not permitted)","Ensure the acting seller owns the product identified by permalink"],"exampleFix":"// components/ProductEdit/ProductTab/ThumbnailEditor.tsx — only attach a blob that finished uploading\nconst upload = await directUpload(file); // throws on its own failure\nif (!upload.signedBlobId) throw new Error(\"Upload did not complete\");\nconst thumbnail = await createThumbnail(permalink, { type: \"file\", signedBlobId: upload.signedBlobId });","handlingStrategy":"validation","validationCode":"const canAttach = (p: ThumbnailPayload) =>\n  typeof p.signedBlobId === \"string\" && p.signedBlobId.length > 0; // never POST an upload that didn't finish\nif (!canAttach(thumbnailPayload)) throw new Error(\"Upload did not complete\");","typeGuard":"import typia from \"typia\";\nconst isThumbnailResponse = typia.is<{ success: true; thumbnail: Thumbnail } | { success: false; error: string }>;","tryCatchPattern":"try {\n  return await createThumbnail(permalink, payload);\n} catch (e) {\n  assertResponseError(e);\n  showError(e.message); // server's refusal: invalid signed_blob_id, bad type, no permission\n}","preventionTips":["Verify the direct upload succeeded before attaching its signed_blob_id","Re-run the full upload (new blob, new signed id) on retry — never reuse the old signed id","Confirm the image type/size before upload to avoid server-side rejection"],"tags":["http","activestorage","direct-upload","thumbnails","validation"],"backgroundTag":"unprocessable-entity-validation","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}