```html

Recovering a Broken Deposit Widget: Diagnosing CORS Failures and Rebuilding Trust in a Production Payment Flow

Last week, the deposit collection widget on JADA's charter event pages went silent. Guests could see the widget, but any attempt to submit a deposit triggered a cascade of CORS errors and silent failures. What should have been a straightforward payment flow became a black hole—no error messages to the user, no clear signal to our team about what had broken. This post walks through how we diagnosed the failure, traced it through three layers of infrastructure, and rebuilt confidence in the system.

The Symptom: Silent Failure on Event Pages

The first signal came from support: guests were abandoning the deposit flow without errors. We checked several event pages directly:


# Test deposit widget on a live event page
curl -s "https://charter-domain.com/event/12345" | grep -i "deposit"

# Inspect network tab for failed requests
# Expected: POST to /api/reservations with guest data
# Actual: XMLHttpRequest to reservation endpoint → CORS preflight fails → silent abort

The widget was rendering, but the underlying fetch call was hitting a CORS wall. No 5xx error. No 4xx rejection. Just a preflight OPTIONS request that returned headers the browser rejected, causing the fetch promise to reject silently in the widget's error handler.

Tracing the Request Path

We started with the live event page source. A typical charter event page includes an embedded deposit widget served from a subdomain, and that widget makes a cross-origin fetch to the reservation endpoint.

Layer 1: Event Page (Primary Domain)

  • File: /var/www/jada-sites/events/charter-page.html (on EC2)
  • Contains: <script src="https://widget.charter-domain.com/deposit-widget.js">
  • Widget origin: https://widget.charter-domain.com

Layer 2: Widget Script (Subdomain)

  • File: /var/www/jada-sites/widgets/deposit-widget.js (same EC2)
  • Makes: POST https://api.charter-domain.com/api/reservations
  • Payload: { charterId, guestEmail, depositAmount, ... }

Layer 3: Reservation Endpoint (API Subdomain)

  • Handler: /var/www/jada-api/routes/reservations.js
  • Lambda backing: shipcaptaincrew function (AWS Lambda)
  • Database: DynamoDB table jada-charters, jada-reservations

The Root Cause: Missing CORS Headers on Read Requests

When we tested the endpoint directly, we found the problem:


# Test preflight request
curl -X OPTIONS https://api.charter-domain.com/api/reservations \
  -H "Origin: https://widget.charter-domain.com" \
  -H "Access-Control-Request-Method: POST" \
  -v

# Response: 200 OK, but missing Access-Control-Allow-Origin header
# Expected: Access-Control-Allow-Origin: https://widget.charter-domain.com
# Actual: (no CORS header at all)

The reservation endpoint was responding to preflight requests, but it wasn't including the necessary CORS headers. Browsers see a successful preflight (200) but no Access-Control-Allow-Origin, and they block the actual POST as a security measure.

Technical Details: Where CORS Was Broken

The Middleware Gap

The API handler in /var/www/jada-api/middleware/cors.js had a conditional that only applied CORS headers to POST requests, not OPTIONS:


// BROKEN: only handles POST
if (req.method === 'POST') {
  res.setHeader('Access-Control-Allow-Origin', 'https://widget.charter-domain.com');
  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}

// OPTIONS requests get a 200 but NO headers → browser blocks the POST

The Fix

We updated the middleware to handle preflight explicitly:


// File: /var/www/jada-api/middleware/cors.js
// (Pseudocode — actual implementation in repo)

const corsOrigins = [
  'https://widget.charter-domain.com',
  'https://charter-domain.com'
];

export function applyCORS(req, res) {
  const origin = req.headers.origin;
  
  if (corsOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '3600');
  }
  
  // Handle preflight
  if (req.method === 'OPTIONS') {
    res.writeHead(204); // No Content
    res.end();
    return true;
  }
  
  return false;
}

We then applied this to the reservation endpoint handler before any business logic runs.

Infrastructure Changes

EC2 Instance: jada-api-prod

  • Region: us-east-1
  • Instance type: t3.medium
  • Files modified: /var/www/jada-api/middleware/cors.js, /var/www/jada-api/routes/reservations.js
  • Restart command:

# SSH into the instance (credentials via AWS Systems Manager Session Manager)
aws ssm start-session --target i-0abc123def456ghi --region us-east-1

# On instance:
sudo systemctl restart jada-api

# Verify the service is up
sudo systemctl status jada-api

CloudFront Distribution

  • Distribution ID: E1JADA2BCDEFG
  • We did NOT need to invalidate the cache for this change, since CORS headers are added by the origin (EC2), not cached by CloudFront
  • CORS headers are considered part of the response body for caching purposes, so they pass through with each request

Route53 DNS

  • No changes needed; api.charter-domain.com was already pointing to the correct origin

Validation and Testing

After the restart, we verified the fix:


# Test preflight again
curl -X OPTIONS https://api.charter-domain.com/api/reservations \
  -H "Origin: https://widget.charter-domain.com" \
  -H "Access-Control-Request-Method: POST