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-consoleView on GitHub (pinned to afeacbd394)
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
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
- 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
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
- paypal_capture_failure
- ${response.message}
- Something went wrong.
- Something went wrong.
- Something went wrong.
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/c8258e2a03cbda20.
Report an issue: GitHub.