TryGhost/Ghost · error · Error

fallbackMessage

Error message

fallbackMessage

What it means

Thrown in handleGiftResponse() (apps/portal/src/utils/api.js) as the final fallback when a gift-endpoint response is non-ok AND HumanReadableError.fromApiResponse(res) returns null (no parseable error envelope). The thrown message is whatever fallbackMessage the caller supplied (e.g. 'Failed to fetch gift redemption data').

Source

Thrown at apps/portal/src/utils/api.js:180

            if (res.ok) {
                return res.json();
            } else {
                throw (await HumanReadableError.fromApiResponse(res)) ?? new Error('Failed to save feedback');
            }
        }
    };

    const handleGiftResponse = async (res, fallbackMessage) => {
        if (res.ok) {
            return res.json();
        }

        const humanError = await HumanReadableError.fromApiResponse(res);
        if (humanError) {
            throw humanError;
        }

        throw new Error(fallbackMessage);
    };

    api.gift = {
        async fetchRedemptionData({token}) {
            const url = endpointFor({type: 'members', resource: `gifts/${encodeURIComponent(token)}/redeem`});
            const res = await makeRequest({
                url,
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                },
                credentials: 'same-origin'
            });

            return handleGiftResponse(res, 'Failed to load gift data');
        },

        async redeem({token}) {

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Inspect the raw response status and body in the Network tab — if it's HTML, the failure is upstream of Ghost (proxy/CDN/firewall).
  2. Confirm the gift token in the redemption URL is unused and unexpired (Settings > Gifts in admin).
  3. Ensure the Ghost server version supports the gift redemption members endpoint.
  4. If proxying, configure it to pass through JSON error envelopes unchanged.

Example fix

// before
const humanError = await HumanReadableError.fromApiResponse(res);
if (humanError) { throw humanError; }
throw new Error(fallbackMessage);

// after: include the status so callers can branch
const humanError = await HumanReadableError.fromApiResponse(res);
if (humanError) { throw humanError; }
const e = new Error(`${fallbackMessage} (${res.status})`);
e.status = res.status;
throw e;
Defensive patterns

Strategy: try-catch

Validate before calling

// validate token shape before fetching
function isValidGiftToken(token) {
    return typeof token === 'string' && token.length >= 16 && /^[A-Za-z0-9_-]+$/.test(token);
}

Try / catch

try {
    const data = await api.gift.fetchRedemptionData({token});
    return data;
} catch (err) {
    // err.message === fallbackMessage; show user-friendly retry
    showGiftError(err.message);
}

Prevention

When it happens

Trigger: Gift fetchRedemptionData / redeem call gets non-ok from {siteUrl}/members/api/gifts/<token>/redeem; the body either is not JSON or has no errors[] array, so HumanReadableError cannot be built, and the generic fallback message is thrown.

Common situations: Gift token invalid, expired, or already redeemed; reverse proxy returning an HTML error page (e.g. Cloudflare 502) so the body has no JSON errors[]; Portal bundle out of sync with backend gift endpoints; member-suppression or rate-limit response without a JSON error body.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/3cce256ebc2defbf. Report an issue: GitHub.