【问题标题】:OPTIONS request inconsistency (in deployment)?OPTIONS 请求不一致(在部署中)?
【发布时间】:2020-07-24 19:44:47
【问题描述】:

简介

所以,我正在使用 MERN 堆栈(使用 Heroku + Netlify),并且在处理 DELETE 请求的方式上遇到了一些非常奇怪的一致性问题。我在最后尝试了无数的解决方案 三天试图让这个工作,他们都没有工作。很多这些解决方案都有 来自堆栈溢出,所以如果你想引导我到另一个帖子,很可能我已经看过了。我已经搜索了网络的每个部分,发布这篇文章是我最后的手段。

问题

因此,当我发出删除请求时,我收到了常规的 OPTIONS 请求,因为我在请求的自定义标头(“x-auth-token”)中发送了一个令牌。 OPTIONS 请求总是以 204 解析,这意味着一切都应该没问题。但是,之后,就没有应有的 DELETE 请求了。这本质上是我的问题。我检查了我的 Heroku 日志,我只能看到 OPTIONS 请求,没有别的。

不一致?

所以这是我一直很困惑的地方。问题是,有时它确实有效。即使我使用相同的中间件,我在 API 中使用的其他路由(例如登录和创建新帖子)也可以正常工作。 每次它工作时,我都会收到 OPTIONS 请求,然后是 DELETE 请求(状态为 200),就像我期望的那样。

如果您想要一个可重现场景的示例:

我在登录并获得有效令牌后创建了 X 个帖子,然后我可以在我的主页上的帖子列表中看到这些帖子呈现。然后,我浏览其中一个帖子并通过单击然后单击确认按钮将其删除。我会自动被重定向到列表中的下一个帖子。我重复这个直到我到达最后一个帖子。我删除了那个帖子,因为没有更多帖子了,我被重定向到帖子列表,它是......不是空的!我尝试删除的最后一个帖子仍然存在。

请记住,所有 DELETE 请求都以完全相同的方式发送,所以我很确定这不是前端问题,因此无需在代码中四处寻找。我已经记录了所有内容并进行了调试,它与我的预期 100% 一致。

(创建帖子不会重定向,而删除帖子会重定向?我看不出这会如何影响 DELETE 请求按照往常发送...尽管可能有解决方案事实。)

我尝试过的解决方案

Cors

首先,您可能已经急于用键盘告诉我这是一个 CORS 问题。我昨天也这么想,但现在我不太确定了。我已经尝试在 CORS 中搞乱所有可能的配置设置以使其正常工作。由于我的两个网站位于不同的域上,因此 CORS 会验证请求。我已经将我的前端网站添加到白名单中,并且所有其他请求都正常通过,所以没有问题。我尝试在配置中添加一个 allowHeaders 选项,但它没有做任何比默认设置更多的事情。我还在配置中的允许方法中添加了“选项”,仍然没有。我也在使用 app.use(cors({config}))。稍后我将包含一些代码以详细了解其中的一些内容。

调试

我基本上已经通过在任何地方插入 console.logs 来测试了,发现中间件、选项路由(我尝试使用相同的路由 url 创建一个选项路由)或原始发布路由都没有在 OPTIONS 请求时执行不会导致 DELETE 请求。

静态服务器

这可能是我缺乏经验的地方(这是我的第一个 Web 项目)。我看到一些解决方案告诉我们需要一个静态服务器。所以我尝试设置一个静态服务器,但我没有看到任何结果。所以我不太确定这完成了什么。

异步和等待

此时我只是在尝试,所以我使所有路由异步,看看它是否会做任何事情。没有。

其他

我还弄乱了环境变量和 dotenv 以及其他我不记得的东西。我想这里的一切应该已经足够了解情况了。

代码

index.js

const express = require('express');
require("dotenv").config({ path: "variables.env" });
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const routes = require("./routes/router");
const cors = require("cors");
const morgan = require('morgan')

const app = express();

const whitelist = [
    process.env.ORIGIN
];

app.use(
cors({
    origin: function (origin, callback) {
        if (whitelist.indexOf(origin) !== -1) {
            callback(null, true);
        } else {
            console.log(origin);
            callback(new Error("Not allowed by CORS"));
        }
    }, //frontend server localhost:3000
    methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    credentials: true, // enable set cookie
}));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(morgan('dev'));



mongoose.connect(process.env.MONGODB_URL, {
    useNewUrlParser: true,
    useCreateIndex: true,
    useUnifiedTopology: true
});

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
  console.log('connected to db');
});

const userSchema = mongoose.Schema({
    name: String,
    password: String
});

// Routes
// TODO: make seperate routers/routes
app.use("/", routes);

// Serve static assets if in production
if (process.env.NODE_ENV === 'production') {
    // Set static folder
    app.use(express.static('client/build'));

    app.get('*', (req, res) => {
        res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
    });
}

// TODO: set up custom port in future
app.listen(process.env.PORT, () => console.log(`Server listening at http://localhost:${process.env.PORT}`));
// Callback functions?

路由器.js

const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
const User = require('../models/User');
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const auth = require('../middleware/auth');
const adminAuth = require('../middleware/adminAuth');
const cors = require("cors");

require("dotenv").config({ path: "variables.env" });
// import 'moment'

// second onwards are handlers => triggers like the post body then next() to go to the next handler
router.post('/api/add_post', adminAuth, async (req, res, next) => {
  try{
    
    newPost = new Post({
      title: req.body.title,
      body: req.body.body,
      author: req.body.author,
      created: req.body.created,
    });

    const savedPost = await newPost.save();
    if (!savedUser) throw Error('Something went wrong saving the post');
    res.send(savedPost);
  } catch (e) {
    res.status(400).json({ msg: e.message });
  }
    
});

router.delete('/api/delete_post/:id', adminAuth, async (req, res, next) => {
  // timeout?
  // console.log(req.body);
  try{
    const id = req.params.id;
    if(!id) throw Error('Invalid ID');
    const post = await Post.findById(id);
    if (!post) throw Error('Post doesn\'t exist');
    const removed = await post.remove();
    if(!removed) throw Error('Problem with deleting the post');
    res.status(200).json({ success: true });
  } catch(e) {
    console.log("Error: ", e.message);
    res.status(400).json({ msg: e.message, success: false });
  }
  
});


// TODO : UPDATE for async soon
router.post('/api/update_post', adminAuth, async (req, res, next) => {
  const id = req.body._id;
  test_post_data = {
      title: req.body.title,
      body: req.body.body,
      author: req.body.author,
      modified: req.body.modified,
  };
  console.log(test_post_data, id);
  Post.updateOne({ _id: id }, test_post_data, (err) => {
      if(err) return next(err);
      return res.status(200);
  });
});

router.get('/api/get_posts', async (req, res, next) => {
  try{
    const posts = await Post.find();
    if(!posts) throw Error('Error with fetching the posts')
    res.send(posts.reverse());
  } catch (e) {
    res.status(400).json({ msg: e.message });
  }
});

router.get('/api/get_chapter/:id', async (req, res, next) => {
  try{
    const id = req.params.id;
    const post = await Post.findOne({_id: id})
    if(!post) throw Error('No post was found')
    res.send(post);
  } catch(e) {
    res.status(400).json({ msg: e.message })
  }
    
});



// User routes
// TODO : make in seperate file

  router.post('/api/user/register', async (req, res) => {
    const { name, email, password } = req.body;
  
    // Simple validation
    if (!name || !email || !password) {
      return res.status(400).json({ msg: 'Please enter all fields' });
    }
  
    try {
      const user = await User.findOne({ email });
      if (user) throw Error('User already exists');
  
      const salt = await bcrypt.genSalt(10);
      if (!salt) throw Error('Something went wrong with bcrypt');
  
      const hash = await bcrypt.hash(password, salt);
      if (!hash) throw Error('Something went wrong hashing the password');
  
      const newUser = new User({
        name,
        email,
        password: hash,
        admin: false
      });
  
      const savedUser = await newUser.save();
      if (!savedUser) throw Error('Something went wrong saving the user');
      

      // TODO : check up on expires stuff : 3600 = 1 hr
      const token = jwt.sign({ id: savedUser._id, admin: savedUser.admin }, process.env.JWT_SECRET, {
        expiresIn: 3600
      });
  
      res.status(200).json({
        token,
        user: {
          id: savedUser.id,
          name: savedUser.name,
          email: savedUser.email,
          admin: savedUser.admin
        }
      });
    } catch (e) {
      res.status(400).json({ error: e.message });
    }
  });

  router.post('/api/user/login', async (req, res) => {

    const { name, password } = req.body;
  
    // Simple validation
    if (!name || !password) {
      return res.status(400).json({ msg: 'Please enter all fields' });
    }
  
    try {
      // Check for existing user
      const user = await User.findOne({ name });
      if (!user) throw Error('User Does not exist');
  
      const isMatch = await bcrypt.compare(password, user.password);
      if (!isMatch) throw Error('Invalid credentials');
  
      const token = jwt.sign({ id: user._id, admin: user.admin }, process.env.JWT_SECRET, { expiresIn: 3600 });
      if (!token) throw Error('Couldnt sign the token');
  
      res.status(200).json({
        token,
        user: {
          id: user._id,
          name: user.name,
          email: user.email,
          admin: user.admin
        }
      });
    } catch (e) {
      res.status(400).json({ msg: e.message });
    }
  });


module.exports = router;

adminAuth.js

const jwt = require('jsonwebtoken')
require("dotenv").config({ path: "variables.env" });

module.exports = (req, res, next) => {
  console.log(req.header('x-auth-token'));
  const token = req.header('x-auth-token');

  // Check for token
  if (!token)
    return res.status(401).json({ msg: 'No token, authorizaton denied' });

  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    console.log('decoded:', decoded);
    
    if(!decoded.admin)
        return res.status(401).json({ msg: 'Not an admin, authorization denied' });

    // Add user from payload
    // console.log('decoded:', decoded);
    req.user = decoded;
    next();
  } catch (e) {
    res.status(400).json({ msg: 'Token is not valid' });
  }
};

请求示例的链接和 Heroku 日志,因为 Stackoverflow 说它是垃圾邮件: https://gist.github.com/macklinhrw/b2fec97642882ba406c49cce3e195c39

编辑

我将 Chrome 请求和响应标头粘贴到底部的 gist 中,但两者都没有响应数据。

我已经使用它进行了一些调试以检查差异,我发现删除操作最终起作用,红色(已取消)请求具有标头,而非工作是完全空的(填充有“临时标头”) ' 如果这意味着什么)。

我无法将请求标头复制粘贴到正在工作的红色(已取消)请求标头的要点中。但是,我粘贴了我认为可能对 chrome 有用的所有内容,希望对您有所帮助。

另外,当我使用 Chrome 网络工具时,我没有看到任何 DELETE 请求,而我在其他工具上看到了它们。不确定它是否重要,可能只是某个地方的配置选项。

【问题讨论】:

  • 如果我们可以从 Chrome 检查器中的网络选项卡中看到返回 OPTIONS 响应的请求的网络跟踪,但没有执行应该在它之后的 DELETE,我们可能可以查看导致浏览器中止 DELETE 操作的 OPTIONS 响应中缺少什么。
  • @jfriend00 好的,我在问题中添加了一些内容并将一些内容粘贴到要点中。我尝试添加您想要的所有内容,但如果缺少任何内容,请告诉我,我会添加更多内容。谢谢!

标签: javascript node.js reactjs express axios


【解决方案1】:

所以,我还没有找到确切的答案,但我找到了解决方法。

事实证明,这可能与axios有关,而我最近3天一直在寻找错误的东西。

这个帖子帮助了我:https://github.com/axios/axios/issues/1428

我在用于删除按钮的 onClick 方法中添加了一个e.preventDefault()。 这解决了问题,但没有重定向(我使用href={link}),所以我要为react-router 添加一个条件渲染来重定向页面。我不知道更好的方法,所以也许给我一些想法。如果我有进一步的麻烦,我会编辑。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-12-27
    • 1970-01-01
    • 2010-09-18
    • 2019-06-26
    • 1970-01-01
    • 2021-02-21
    • 2016-12-29
    • 2018-01-13
    相关资源
    最近更新 更多