{"record":{"id":"c8258e2a03cbda20","repo":"antiwork/gumroad","slug":"server-returned-error-response","errorCode":null,"errorMessage":"Server returned error response.","messagePattern":"Server returned error response\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/javascript/data/paypal.ts","lineNumber":39,"sourceCode":"    postal_code?: string;\n    recipient_name?: string;\n    state?: string;\n  };\n};\n\nexport const createBillingAgreement = async (billingAgreementTokenId: string): Promise<BillingAgreement> => {\n  try {\n    const response = await request({\n      method: \"POST\",\n      url: Routes.billing_agreement_paypal_path(),\n      accept: \"json\",\n      data: { billing_agreement_token_id: billingAgreementTokenId },\n    });\n\n    if (response.ok) {\n      return typia.assert<BillingAgreement>(await response.json());\n    }\n    throw new Error(\"Server returned error response.\");\n  } catch (e) {\n    // eslint-disable-next-line no-console\n    console.error(\"Error creating a PayPal billing agreement\", e);\n    throw e;\n  }\n};\n\nexport const createBillingAgreementToken = async (data: { shipping: boolean }): Promise<string> => {\n  try {\n    const response = await request({\n      url: Routes.billing_agreement_token_paypal_path(data),\n      method: \"POST\",\n      accept: \"json\",\n    });\n    const responseData = typia.assert<{ billing_agreement_token_id: string }>(await response.json());\n    return responseData.billing_agreement_token_id;\n  } catch (e) {\n    // eslint-disable-next-line no-console","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/paypal.ts#L21-L57","documentation":"A plain Error('Server returned error response.') thrown in paypal.ts:39 when the POST to Routes.billing_agreement_paypal_path() (exchanging a PayPal billing-agreement token for a stored BillingAgreement) answers non-ok. Unlike the rest of the data layer this throws Error, not ResponseError, and the surrounding catch logs 'Error creating a PayPal billing agreement' with the error to console.error before rethrowing, so the console carries the only detail. Non-ok here means 4xx (request() already converts 5xx/network to ResponseError).","triggerScenarios":"The billing_agreement_token_id is expired (PayPal tokens live ~3 hours), already used (single-use), or was generated for a different PayPal environment/mode than the server uses; the seller's PayPal REST credentials are missing or wrong server-side; PayPal itself declined agreement creation; or the user's PayPal session/onboarding flow was abandoned mid-way.","commonSituations":"User leaves the PayPal approval window open overnight and returns to click 'agree' with a dead token; sandbox vs live credential mismatch between the JS flow and the server's PayPal keys; seller in a country where reference transactions/billing agreements are not enabled; retrying createBillingAgreement with a token whose preceding createBillingAgreementToken call already consumed it.","solutions":["Check the console output first — the catch block logs the full error including status before rethrowing","Regenerate the token via createBillingAgreementToken and retry the agreement creation once; tokens are single-use and short-lived","Verify server-side PayPal credentials and mode (sandbox/live) match the client that produced the token id","Parse the 4xx JSON body before throwing so PayPal's decline reason reaches the user instead of a generic string","If the seller's account cannot create billing agreements (region/permission), surface that as an account-setup message rather than a transient error"],"exampleFix":"// before\nif (response.ok) {\n  return typia.assert<BillingAgreement>(await response.json());\n}\nthrow new Error('Server returned error response.');\n\n// after — throw ResponseError with the server's reason so callers can narrow it\nif (response.ok) return typia.assert<BillingAgreement>(await response.json());\nconst body = await response.json().catch(() => null) as { error?: string } | null;\nthrow new ResponseError(body?.error ?? 'Server returned error response.');","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"// Note: this site throws Error, not ResponseError — narrow accordingly\nconst isPaypalAgreementError = (e: unknown): boolean =>\n  e instanceof Error && /Server returned error response/i.test(e.message);","tryCatchPattern":"try {\n  return await createBillingAgreement(billingAgreementTokenId);\n} catch (e) {\n  if (e instanceof DOMException && e.name === 'AbortError') throw e;\n  // token may be stale: regenerate once via createBillingAgreementToken, then give up\n  const fresh = await createBillingAgreementToken({ shipping: true });\n  return await createBillingAgreement(fresh);\n}","preventionTips":["Generate the billing-agreement token only when the user starts the PayPal flow","Complete createBillingAgreement immediately after user approval — tokens are single-use and short-lived","Watch console: this path logs 'Error creating a PayPal billing agreement' with the underlying response"],"tags":["paypal","billing-agreement","http-4xx","payments"],"backgroundTag":"paypal-billing-agreement-failed","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}