javascriptcorsstripe-paymentsfetch-apiflask-wtforms

No 'Access-Control-Allow-Origin' header is present on the requested resource - trying to collect WTForms data and redirect to stripe from javascript


My objective: I am developing a website (HTML, Boostrap, Javascript) where the user can add and fill an arbitrary number of forms. The user then submits these forms, and they are processed in the backend (Python, Flask). The forms are WTForms, and they are validated in the backend before being added to a database. Finally, the user is redirected to a stripe-hosted stripe checkout page.

My progress: I have successfully managed to allow the user to fill an arbitrary number of forms which are then posted as a list of json objects. In the backend, I convert them back to WTForms (I think this is right), validate them, and add them to a database.

My issue: I am struggling with the last part of my objective, ie redirecting the user to a stripe-hosted stripe checkout page. I am getting the following CORS-related issue:

Access to fetch at 'https://checkout.stripe.com/c/pay/cs_test_a1SrbQBbElmNFdfuSsF...' (redirected from 'http://localhost:4242/test') from origin 'http://localhost:4242' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

I have done quite a bit of reading and found this helpful stackoverflow post (No 'Access-Control-Allow-Origin' header is present on the requested resource—when trying to get data from a REST API), but all of this is quite new to me and I don't fully understand where the issue is coming from. Most importantly, I don't understand how to fix it, nor if there are easier ways to achieve my initial objective or if I'm making my life too difficult in some ways.

The following are the related segments of my code so far:

Frontend (related script inside test.html file):

<script>
  document.addEventListener('DOMContentLoaded', () => {
    var button = document.getElementById('submit');    
    // Add event listener for button click
    button.addEventListener('click', event => {
      var wholeJson = {};
      if(productId.trim === '') {
        console.log('product id is empty but it shouldn\'t');
        return;
      }
      wholeJson['product_name'] = productId;
      wholeJson['forms'] = [];

      const forms = document.querySelectorAll('form');
          
      // Gather data from all forms
      forms.forEach(form => {
        const formData = new FormData(form);

        // Convert FormData to JSON manually
        var jsonForm = {};
        formData.forEach((value, key) => {
            jsonForm[key] = value;
        });

        wholeJson['forms'].push(jsonForm);
      });
      
      // Send data from all forms to the server
      fetch('/test', {
        method: 'POST',
        body: JSON.stringify(wholeJson), // Convert form data to JSON
        headers: {
          'Content-Type': 'application/json'
        }
      }).then(response => {
        console.log('here');
        // Handle response from server if needed
        if (response.ok) {
          console.log('response ok');
          // Redirect or perform other actions
          window.location.href = response.url;
        } else {
          console.log('response NOT ok');
          // Handle error
          console.error('Server responded with an error:', response.statusText);
        }
      }).catch(error => {
        console.log('error');
        // Handle fetch error
        console.error('Error:', error);
      });
    });
  });
</script>

Backend:

import requests
import stripe
from datetime import datetime, timedelta
from flask import Flask, session, request, url_for, redirect
from wtforms import StringField, DateField
from flask_wtf import FlaskForm

class TestForm(FlaskForm):
    fieldA = StringField(label='Field A', validators=[DataRequired()])
    fieldB = StringField(label='Field B', validators=[DataRequired()])
    date = DateField(label='Date', validators=[DataRequired()], default=(datetime.now() + timedelta(days=1)).date())

@app.route('/test', methods=["GET", "POST"])
def test():
    if request.method == "POST":
        # Handle form submission
        product_name : str = request.json['product_name']
        product_id = None
        if product_name in products_to_id:
            product_id = products_to_id[product_name]
        session['product_id'] = product_id
        
        session['json_tests'] = []
        forms = request.json['forms']
        validForms: bool = True
        for formJson in forms:
            form_input = ImmutableMultiDict(formJson)
            test_form: TestForm = TestForm(form_input)
            if test_form.validate_on_submit():
                
                date = datetime.strptime(formJson["date"], date_format).date()

                # create Test object from json (unrelated)
                test = Test(
                    fieldA=formJson["fieldA"],
                    fieldB=formJson["fieldB"],
                    date=date
                )
                session['json_tests'].append(test.to_json())

            else:
                print(test_form.errors)
                validForms = False
                break
        
        if validForms:
            return redirect(url_for('create_checkout_session'))
    
    test_form = TestForm()
    return render_template("test.html", form=test_form)

@app.route('/create_checkout_session', methods=["GET", "POST"])
def create_checkout_session():
    try:
        product_id = session.get('product_id')
        domain_url = os.getenv('DOMAIN')

        checkout_session = stripe.checkout.Session.create(
            line_items=[
                {
                    'price': product_id,
                    'quantity': 1,
                },
            ],
            mode='subscription',
            success_url=domain_url + url_for('success'),
            cancel_url=domain_url + '/cancel',
        )
    except Exception as e:
        return str(e)

    return redirect(checkout_session.url, code=303)


Solution

  • The issue is that your backend is attempting to do a HTTP redirect, which is not compatible with the fetch/AJAX approach your frontend code is using(you can not redirect from a fetch).

    The most straightforward way to fix it is to change the backend to do something like this

    return jsonify(url=checkout_session.url)

    instead of return redirect(checkout_session.url, code=303)

    and then on the frontend you can access the URL and redirect the same way you're mostly doing already.

    if (response.ok) {
        console.log('response ok');
        response.json().then(respJson => {
          // Redirect or perform other actions
           window.location.href = respJson.url;
        })
    }