{"record":{"id":"f7dddc53741ad864","repo":"TryGhost/Ghost","slug":"failed-to-process-gift-checkout-please-try-again","errorCode":null,"errorMessage":"Failed to process gift checkout, please try again.","messagePattern":"Failed to process gift checkout, please try again\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/portal/src/utils/api.js","lineNumber":677,"sourceCode":"                },\n                body: JSON.stringify(body)\n            });\n\n            let responseJson = {};\n            try {\n                responseJson = await response.json();\n            } catch (e) {\n                // response may not be JSON (e.g. HTML error page from proxy)\n            }\n\n            if (!response.ok) {\n                const error = responseJson?.errors?.[0];\n\n                if (error) {\n                    throw error;\n                }\n\n                throw new Error('Failed to process gift checkout, please try again.');\n            }\n\n            if (responseJson.url) {\n                return window.location.assign(responseJson.url);\n            }\n\n            throw new Error('Failed to process gift checkout, please try again.');\n        },\n\n        async checkoutDonation({successUrl, cancelUrl, metadata = {}, personalNote = ''} = {}) {\n            const identity = await api.member.identity();\n            const url = endpointFor({type: 'members', resource: 'create-stripe-checkout-session'});\n\n            const metadataObj = {\n                fp_tid: (window.FPROM || window.$FPROM)?.data?.tid,\n                urlHistory: getUrlHistory(),\n                ...metadata\n            };","sourceCodeStart":659,"sourceCodeEnd":695,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/portal/src/utils/api.js#L659-L695","documentation":"Thrown by checkoutGift() at line 677: the HTTP response was not ok AND the body had no structured errors[0] to rethrow. checkoutGift defensively parses the body as JSON (catching parse failures, e.g. an HTML proxy error page), so this branch fires when the backend returned a non-2xx with either an empty body or a JSON shape lacking errors[]. It is a 'something failed and we have no detail' signal.","triggerScenarios":"Reverse proxy/CDN returns a 502/503 HTML page (not JSON) in front of Ghost; Ghost returns a non-2xx with an empty body during an unhandled crash; a WAF blocks the gift-checkout POST and returns a plain-text challenge; the response body parse succeeded but didn't contain the expected errors[] envelope.","commonSituations":"Cloudflare/nginx in front of Ghost timing out on a slow Stripe session-creation call; Ghost worker crashed mid-request (OOM) leaving a truncated response; gift tier/duration combination rejected by a custom validation that doesn't return the standard error envelope; misconfigured base path so the POST hits a 404 HTML page.","solutions":["Open browser devtools → Network, find the create-stripe-checkout-session POST, and inspect the raw response status and body — the status code (502/504/404) localizes the layer.","If a proxy returned HTML, fix the proxy timeout/routing to reach Ghost's members API.","Check Ghost server logs around the request timestamp for a crash or unhandled error.","Confirm the tierId/duration/cadence sent are valid for the site's configured gift tiers.","Ensure the Portal is built against a Ghost backend whose members API returns the standard errors[] envelope."],"exampleFix":"// before: empty fallback hides the HTTP status\nif (!response.ok) {\n    const error = responseJson?.errors?.[0];\n    if (error) throw error;\n    throw new Error('Failed to process gift checkout, please try again.');\n}\n\n// after: include status + snippet so triage is possible\nif (!response.ok) {\n    const error = responseJson?.errors?.[0];\n    if (error) throw error;\n    throw new Error(`Failed to process gift checkout (HTTP ${response.status}). Please try again.`);\n}","handlingStrategy":"try-catch","validationCode":"// Pre-validate the gift inputs before posting\nfunction validGiftInput({tierId, duration, cadence}) {\n    return (typeof tierId === 'string' && tierId.length > 0) &&\n        (duration !== undefined || (cadence === 'monthly' || cadence === 'yearly'));\n}","typeGuard":"function hasStructuredError(body) {\n    return body && Array.isArray(body.errors) && body.errors[0];\n}","tryCatchPattern":"try {\n    await api.member.checkoutGift({tierId, duration, cadence, email});\n} catch (err) {\n    // No structured detail from backend — show generic + log status for triage\n    showToast(err.message);\n    console.error('gift checkout failed', err);\n}","preventionTips":["Ensure the network path from Portal to Ghost's members API is clean (no HTML-error proxies).","Keep Portal and Ghost backend versions aligned so the error envelope matches.","Log the HTTP status alongside the message when the backend gives no detail."],"tags":["portal","stripe","gift-checkout","network","error-envelope"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}