antiwork/gumroad · error · Error

Server returned error response.

Error message

Server returned error response.

What it means

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).

Source

Thrown at app/javascript/data/paypal.ts:39

    postal_code?: string;
    recipient_name?: string;
    state?: string;
  };
};

export const createBillingAgreement = async (billingAgreementTokenId: string): Promise<BillingAgreement> => {
  try {
    const response = await request({
      method: "POST",
      url: Routes.billing_agreement_paypal_path(),
      accept: "json",
      data: { billing_agreement_token_id: billingAgreementTokenId },
    });

    if (response.ok) {
      return typia.assert<BillingAgreement>(await response.json());
    }
    throw new Error("Server returned error response.");
  } catch (e) {
    // eslint-disable-next-line no-console
    console.error("Error creating a PayPal billing agreement", e);
    throw e;
  }
};

export const createBillingAgreementToken = async (data: { shipping: boolean }): Promise<string> => {
  try {
    const response = await request({
      url: Routes.billing_agreement_token_paypal_path(data),
      method: "POST",
      accept: "json",
    });
    const responseData = typia.assert<{ billing_agreement_token_id: string }>(await response.json());
    return responseData.billing_agreement_token_id;
  } catch (e) {
    // eslint-disable-next-line no-console

View on GitHub (pinned to afeacbd394)

Solutions

  1. Check the console output first — the catch block logs the full error including status before rethrowing
  2. Regenerate the token via createBillingAgreementToken and retry the agreement creation once; tokens are single-use and short-lived
  3. Verify server-side PayPal credentials and mode (sandbox/live) match the client that produced the token id
  4. Parse the 4xx JSON body before throwing so PayPal's decline reason reaches the user instead of a generic string
  5. If the seller's account cannot create billing agreements (region/permission), surface that as an account-setup message rather than a transient error

Example fix

// before
if (response.ok) {
  return typia.assert<BillingAgreement>(await response.json());
}
throw new Error('Server returned error response.');

// after — throw ResponseError with the server's reason so callers can narrow it
if (response.ok) return typia.assert<BillingAgreement>(await response.json());
const body = await response.json().catch(() => null) as { error?: string } | null;
throw new ResponseError(body?.error ?? 'Server returned error response.');
Defensive patterns

Strategy: try-catch

Type guard

// Note: this site throws Error, not ResponseError — narrow accordingly
const isPaypalAgreementError = (e: unknown): boolean =>
  e instanceof Error && /Server returned error response/i.test(e.message);

Try / catch

try {
  return await createBillingAgreement(billingAgreementTokenId);
} catch (e) {
  if (e instanceof DOMException && e.name === 'AbortError') throw e;
  // token may be stale: regenerate once via createBillingAgreementToken, then give up
  const fresh = await createBillingAgreementToken({ shipping: true });
  return await createBillingAgreement(fresh);
}

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/c8258e2a03cbda20. Report an issue: GitHub.