0

一日一技:Bug分析,假删除导致文章发布成功却打不开的问题

 3 months ago
source link: https://www.kingname.info/2022/06/20/fake-delete/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一日一技:Bug分析,假删除导致文章发布成功却打不开的问题

2022-06-20

|

后端

|

0

公司有一个内部博客,大家可以在上面创建自己的账号,然后写文章在全公司分享。昨天这个内部博客开通了API,因此我准备写一个Python程序,把自己公众号文章都搬运上去。

然后我就发现这个API接口有一个bug。并且根据它的现象,猜到它问题出在哪里。

我先来简单描述一下现象。

假设我硬盘上现在有50个Markdown文件。现在我要把它发布到网站上。简化代码如下:

import glob
import requests

for path in glob.glob('blog/*.md'):
with open(path) as f:
article = f.read()
requests.post('https://xxx.yyy.com/post?token=abcasdf', json={'content': content})

发布完成以后,文章确实都已经在网页上出现了,并且每篇文章都能正常显示。但我粗略浏览了一下,发现里面有一些文章的末尾带上来我的微信公众号二维码。我不想让公司的人知道我的公众号,所以准备修改一下文章。

有一些文章有二维码,有一些没有,一个一个改起来很麻烦,所以我做了两步操作。首先写了一个程序,扫描所有Markdown文件,发现二维码就删掉。然后,我直接在网站上把刚刚发布的所有文章都删了(懒得去看哪篇有二维码,哪篇没有,干脆全删了重发)。

接下来,我再次运行程序批量重新发布文章。2秒钟以后发布完成。

本来一切看起来都很正常,但是当我到网站上查看的时候,发现有很多文章点开以后,都提示『该文章已经删除』。

我一开始在想是不是我的程序写的不对,漏掉了这些文章。我重新单独一篇一篇发布这篇文章,API接口返回发布成功,可在网页上还是显示文章已经删除。

然后我一篇一篇检查这些发布失败的文章,发现有一个共同的特点:他们是一开始就没有二维码的文章。相当于这些文章我在网站上删除以后原样重新又发了一次。

那我就有了一个初步的猜测,大概知道原因是什么了:

  1. 因为每篇文章有一个docid,当第一次发布文章的时候,这个docid就是文章正文内容的md5值。只要文章完全一样,连续发多少次,它的docid都一样。这样就可以防止出现重复文章。(更新的时候,需要用户主动提供docid,避免重新生成新的)。
  2. 这个网站的删除功能,肯定是假删除。也就是当我点了删除文章的按钮时,文章其实依然在数据库里面,只不过增加了一个字段removed=True。网页显示文章的时候,查询条件肯定是col.find({'removed': {"$ne": True}}),所以就不会把这些被软删除的文章显示出来。
  3. API发布新文章的时候,肯定使用的是更新操作。并且使用了upsert=True

以MongoDB为例,这个API背后的逻辑肯定是这样的:

def post_article(docid, article_info):
mongo.update_one({'_id': docid}, {'$set': article_info}, upsert=True)

upsert=True的作用,是先检查数据是否存在,如果存在就更新,如果不存在就插入。

第一次发布的时候,文章不存在,直接插入,正常。如果用户正常使用修改接口,修改了正文,因为用户主动提供了docid,所以也能正常更新。

但如果用户先删除了数据,此时数据库中,增加了一个字段removed=True。然后用户又原封不动重新发一次文章。那么docid肯定还是原来那个。这条文章已经在数据库中存在了。于是逐一更新了每个字段。但是新发布的字段里面是没有removed这个字段的,所以更新的时候不会更新它,它还在数据库里面。所以就出现了发布成功,但是打开新闻又提示文章已经删除。

我去问了一下做这个API的同学,果然它的bug原因跟我设想的一模一样。

这个bug解决方法非常简单,发布新文章的时候,把update_one改成replace_one就可以了:

def post_article(docid, article_info):
mongo.replace_one({'_id': docid}, {'$set': article_info}, upsert=True)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK