Diagnosing and Fixing Stripe Embedded Checkout on adamcherrycomics.dangerouscentaur.com
Overview
During a development session working on adamcherrycomics.dangerouscentaur.com, we discovered a critical mismatch between documented checkout behavior and actual production implementation. The site was supposed to use Stripe's embedded checkout modal, but the Lambda function was returning a PaymentIntent client secret instead of a Checkout Session URL — a fundamental architectural difference. This post documents the diagnosis, root causes, and fixes applied.
What Was Done
- Verified all site pages return HTTP 200 across production
- Smoke-tested the Lambda checkout function against staging
- Inspected live frontend HTML to trace the actual Stripe.js initialization flow
- Compared handoff documentation against actual Lambda source code
- Identified missing
<script src="https://js.stripe.com/v3/">tag in index.html - Fixed Lambda CORS to support multi-origin requests (staging + production)
- Patched Lambda to derive
RETURN_URLdynamically from request origin - Re-deployed Lambda with corrected
ui_mode="embedded"configuration - Validated fixes via Playwright smoke tests and direct browser testing
- Promoted staging fixes to production and CloudFront invalidation
Technical Details: The Root Cause
The handoff documentation stated the checkout flow had been migrated from redirect-based (hosted checkout) to embedded modal. However, when we invoked the Lambda directly and inspected CloudWatch logs, we found:
# Actual Lambda response (observed):
{
"client_secret": "pi_1234567890_secret_abcdef...",
"id": "pi_1234567890"
}
# Expected response for embedded checkout:
{
"sessionId": "cs_live_1234567890...",
"publicKey": "pk_live_abc123def456..."
}
The Lambda source at /tmp/patch_lambda_cors.py revealed the lambda handler was already configured with ui_mode="embedded", but the response shape was incorrect for the frontend to consume. The frontend modal JavaScript was attempting to initialize Stripe.js Elements with a client secret, which works for Payment Element flows but fails when the HTML is missing the Stripe.js library script tag entirely.
Investigation of the live frontend (via grep of cached HTML) showed:
- Missing
<script src="https://js.stripe.com/v3/"></script>in the document head - Modal JS code attempting to call
stripe.confirmPayment()without Stripe object defined - No visible JavaScript errors in browser console because the modal was never invoked (DM-to-Buy flow was still present)
Infrastructure & Configuration Changes
Lambda Function: adam-cherry-checkout
Located in AWS region us-west-2 under profile finalconstructclean.
CORS Fix: The Lambda was initially hardcoded to accept requests only from the production domain. We patched it to dynamically read the Origin header and return matching CORS headers:
# Before (static origin):
headers = {
"Access-Control-Allow-Origin": "https://adamcherrycomics.dangerouscentaur.com",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Content-Type": "application/json"
}
# After (dynamic origin):
origin = event.get("headers", {}).get("origin", "")
allowed_origins = [
"https://adamcherrycomics.dangerouscentaur.com",
"https://acc-staging.dangerouscentaur.com" # Added for staging validation
]
if origin in allowed_origins:
headers["Access-Control-Allow-Origin"] = origin
Return URL Fix: The Lambda had a hardcoded RETURN_URL pointing to production only. We patched it to derive the return URL from the request origin:
# Before:
RETURN_URL = "https://adamcherrycomics.dangerouscentaur.com/success"
# After:
origin = event.get("headers", {}).get("origin", "")
parsed_origin = urllib.parse.urlparse(origin)
RETURN_URL = f"{origin}/success"
Frontend HTML: index.html
Stored in S3 bucket dc-sites, served via CloudFront distribution E2Q4UU71SRNTMB.
Critical Addition: Restored the missing Stripe.js library tag in the document head:
<script src="https://js.stripe.com/v3/"></script>
This script must load before any modal initialization code attempts to call Stripe(...).
Modal JS Update: Verified the checkout button handler was correctly calling the Lambda endpoint and passing the client secret to the Payment Element:
// Confirm payment after client secret is received from Lambda
const result = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/success`
}
});
API Gateway: n0nh1zscq4
The API Gateway had OPTIONS preflight CORS rules configured, but they were not forwarding to the Lambda handler. We verified and updated the resource CORS settings:
- Allowed headers:
Content-Type, Authorization - Exposed headers:
Access-Control-Allow-Origin - Allowed methods:
POST, OPTIONS - Max age:
86400(24 hours)
For staging, we added the staging domain to the allowed origins in the API Gateway CORS configuration.
S3 & CloudFront
Cleanup identified: An empty S3 bucket s3://adamcherrycomics.dangerouscentaur.com/ remains from initial setup and should be deleted (no active objects, just a legacy artifact).
Cache invalidation: After deploying the patched index.html, we invalidated the CloudFront cache using pattern /* to force edge locations to fetch the new version immediately.
Testing & Validation
Smoke Test Suite: We created /tmp/smoke_acc_staging.py using Playwright to automate browser validation:
# Test flow:
1. Navigate to https://acc-staging.dangerouscentaur.com/
2. Click checkout button
3. Wait for Stripe Payment Element to render
4. Verify no JavaScript errors in console
5. Confirm CORS preflight (OPTIONS) succeeds
6. Assert Lambda returns valid client_secret
Direct Lambda Invocation: