Reputation: 287
I'm having trouble understanding why I'm getting a Cross-Origin Request Blocked error when trying to send a user in my app to a sandbox.paypal.com payment page when ordering a product. The frontend of my app (with React) uses localhost port 3000 while the backend uses localhost port 4000. The communication between the two ports when performing CRUD operations works as intended. But now that I'm introducing paypal into the mix the app doesn't go to the sandbox paypal page when trying to order a product. This is the error message in the console:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-0Y510528E2479935T. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
I'm confused because communication already works between the two localhosts. Doesn't Access-Control-Allow-Origin default to "*"? I'm using "paypal-rest-sdk" in Node.js
Frontend logic:
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { useID, useAdmin } from "../../context/auth";
import { Button, Accordion, Card, ListGroup, Form } from "react-bootstrap";
import axios from "axios";
function ProductDetails(props) {
const [isError, setIsError] = useState(false);
const [id, setID] = useState("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [price, setPrice] = useState(0);
const [stock, setStock] = useState(0);
const [messages, setMessages] = useState([]);
const { IDTokens } = useID();
const { adminTokens } = useAdmin();
const Message = props => (
<Card>
<Card.Body>
<Card.Title>
{props.message.owner.username === "(User removed)" ? (
<span>{props.message.owner.username}</span>
) : (
<Link to={`/users/${props.message.owner.id}`}>{props.message.owner.username}</Link>
)}
</Card.Title>
<Card.Text>
{props.message.content}
</Card.Text>
{IDTokens === props.message.owner.id || adminTokens ? (
<span>
<Link to={`/products/list/${id}/messages/${props.message._id}/edit/`} style={{ marginRight: 10, marginLeft: 10 }}>
Edit
</Link>
<Link to={`/products/list/${id}/messages/${props.message._id}/delete/`}>Delete</Link>
</span>
) : (
<span></span>
)}
</Card.Body>
</Card>
)
useEffect(() => {
axios.get(`http://localhost:4000/products/${props.match.params.id}`)
.then(res => {
setID(res.data.product._id);
setName(res.data.product.name);
setDescription(res.data.product.description);
setPrice(res.data.product.price);
setStock(res.data.product.stock);
setMessages(res.data.messages);
}).catch(function(err) {
setIsError(true);
})
}, [props, IDTokens]);
function messageList() {
return messages.map(function(currentMessage, i){
return <Message message={currentMessage} key={i} />;
})
}
function postOrder() {
if(stock > 0) {
let productInfo = {
name,
description,
price
};
axios.post("http://localhost:4000/orders/pay",
productInfo
).then(res => {
if(res.status === 200) {
console.log(res.data);
} else {
setIsError(true);
}
}).catch(err => {
setIsError(true);
});
}
}
return (
<div className="text-center">
<h2>Products Details</h2>
<Accordion>
<Card>
<Card.Header>
<Accordion.Toggle as={Button} variant="link" eventKey="0">
Product Info
</Accordion.Toggle>
</Card.Header>
<Accordion.Collapse eventKey="0">
<Card.Body>
<ListGroup>
<ListGroup.Item>Name: {name}</ListGroup.Item>
<ListGroup.Item>Description: {description}</ListGroup.Item>
<ListGroup.Item>Price: ${price.toFixed(2)}</ListGroup.Item>
<ListGroup.Item>Stock: {stock}</ListGroup.Item>
</ListGroup>
{stock > 0 ? (
<Form>
<Button onClick={postOrder} variant="success">Order Now</Button>
{ isError &&<p>Something went wrong with making the order!</p> }
</Form>
) : (
"Cannot order, currently out of stock"
)}
</Card.Body>
</Accordion.Collapse>
</Card>
</Accordion>
<Link to={`/products/list/${id}/messages/new`}>Questions or Comments Regarding this Product? Leave a Message.</Link>
<h3>Messages: </h3>
{messages.length > 0 ? (
messageList()
) : (
<p>(No messages)</p>
)}
{ isError &&<p>Something went wrong with getting the product!</p> }
</div>
)
}
export default ProductDetails;
Backend logic:
const express = require("express"),
router = express.Router(),
paypal = require("paypal-rest-sdk"),
Order = require("../database/models/order");
router.post("/pay", function(req, res) {
console.log("req.body: ", req.body);
const create_payment_json = {
"intent": "sale",
"payer": {
"payment_method": "paypal"
},
"redirect_urls": {
"return_url": "http://localhost:3000/orders/success",
"cancel_url": "http://localhost:3000/orders/cancel"
},
"transactions": [{
"item_list": {
"items": [{
"name": req.body.name,
"sku": "001",
"price": req.body.price,
"currency": "USD",
"quantity": 1
}]
},
"amount": {
"currency": "USD",
"total": req.body.price
},
"description": req.body.description
}]
};
paypal.payment.create(create_payment_json, function (err, payment) {
if(err) {
console.log(err.message);
} else {
for(let i = 0; i < payment.links.length; i++){
if(payment.links[i].rel === 'approval_url'){
res.redirect(payment.links[i].href);
}
}
}
});
});
router.get('/success', (req, res) => {
console.log("req.query: ", req.query)
const payerId = req.query.PayerID;
const paymentId = req.query.paymentId;
const execute_payment_json = {
"payer_id": payerId,
"transactions": [{
"amount": {
"currency": "USD",
"total": req.query.total
}
}]
};
paypal.payment.execute(paymentId, execute_payment_json, function (error, payment) {
if(err) {
console.log(err.message);
} else {
let order = new Order(JSON.stringify(payment));
order.save().then(order => {
res.status(200).json(`Order added successfully! Created order details: ${order}`);
}).catch(err => {
console.log("Order create error: ", err.message);
});
}
});
});
server.js (client_id and client_secret are changed for stackoverflow):
const express = require("express"),
app = express(),
bodyParser = require("body-parser"),
mongoose = require("mongoose"),
session = require("express-session"),
passport = require("passport"),
localStrategy = require("passport-local"),
paypal = require("paypal-rest-sdk"),
cors = require("cors"),
PORT = 4000,
// Require models
User = require("./database/models/user"),
// Require routes
productRoutes = require("./routes/products"),
messageRoutes = require("./routes/messages"),
orderRoutes = require("./routes/orders"),
userRoutes = require("./routes/users");
app.use(bodyParser.json());
app.use(cors());
// Paypal config
paypal.configure({
"mode": "sandbox", //sandbox or live
"client_id": "...",
"client_secret": "..."
});
// Mongoose config
mongoose.set('useUnifiedTopology', true);
mongoose.set('useFindAndModify', false);
mongoose.connect("mongodb://localhost/barnwood", {
useNewUrlParser: true,
useCreateIndex: true
});
// Sessions
app.use(
session({
secret: "Birdhouses are cool.", // Secret can be any string
resave: false,
saveUninitialized: false
})
);
app.use(passport.initialize());
app.use(passport.session());
passport.use(new localStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
// Routes config
app.use("/products", productRoutes);
app.use("/messages", messageRoutes);
app.use("/orders", orderRoutes);
app.use("/users", userRoutes);
// Start server
app.listen(PORT, function() {
console.log("Server is running on Port: " + PORT);
});
Upvotes: 5
Views: 5439
Reputation: 745
You're asking your backend server to process the payment and upon success your backend server is doing res.redirect(payment.links[i].href);
your browser (axios which has maxRedirects set to 5 by default) is going to follow the redirect and then read the response which is on a different domain than yours, which PayPal refuses you to read in a cross origin way. Reason why you get CORS being blocked.
You have two solutions to this problem:
res.redirect(payment.links[i].href);
you should reply the link and let the browser redirect.For example:
// replace res.redirect(payment.links[i].href); by
res.json({forwardLink: payment.links[i].href});
Then in your React app, you should read the response and do a window.location = response.forwardLink
axios
.post('http://localhost:4000/orders/pay', productInfo)
.then((res) => {
if (res.status === 200) {
console.log(res.data)
} else {
setIsError(true)
}
})
.catch((err) => {
setIsError(true)
})
becomes
axios
.post('http://localhost:4000/orders/pay', productInfo)
.then((res) => {
if (res.status === 200) {
console.log(res.data)
window.location = res.data.forwardLink
} else {
setIsError(true)
}
})
.catch((err) => {
setIsError(true)
})
maxRedirects: 0
I think), in which case your response code would be 302
(instead of 200) and you could read the headers.location
param with which you can then do window.location = headers.location
Your code would become something like:
axios({
maxRedirects: 0,
method: 'post',
url: 'http://localhost:4000/orders/pay',
data: productInfo,
})
.then((res) => {
if (res.status === 302) {
console.log(res.headers)
window.location = res.headers.location
} else {
setIsError(true)
}
})
.catch((err) => {
setIsError(true)
})
Upvotes: 4