You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
the thing is that since you need to operate on them and they are case-insensitive you need to either upper or lower case them. The latter is the case of nodejs. Someone as to do it, in this case nodejs is saving you that step IMO. Also applications shouldn't rely in the case of those fields since, again, they are case-insensitive.
One example : Proxy.
If, as I was developing, you are proxying between a client and a server (two couchdb instances replicating in that case) for which case is important (it is probably wrong, but you can't modify the code) : the client didn't understand the answer if the headers' case was not the original one. In that case, you need to forward headers exactly as received.
The responseURL getter steps are to return the empty string if this’s response’s URL is null; otherwise its serialization with the exclude fragment flag set.
所以,如果你一定要终止重定向请求,那么可以通过 responseURL 和原始的请求 URL 进行对比,如果不同,则表明存在重定向,但是不推荐使用这种逻辑判断,因为这不是官方标准。另外,这里的 status 取到的是重定向后的值,所以不能用它对比。
前言
前段时间,项目在进行 JWT 改造,用户的身份认证从
cookie
改成了token
。上线之后,用户反馈了一个 Bug:
询问了一些基本信息之后,发现他用的浏览器是 Safari,然后我按照他的操作路径模拟了一遍,果然复现了。
你可以打开这个在线地址,点击按钮,在 Chrome 中会正常展示数据,在 Safari 中会提示
request error
。现象描述
经过调试之后发现,是因为有一个接口由于请求地址不对,接口返回了
301
,需要重定向到新的接口:/api/user/list
/api/user/list-new
在 Safari 中具体请求如下(Safari 自动将原请求和重定向合并为了 1 个请求记录):
当浏览器收到
3XX
的重定向状态码后,会自动对新的地址发起请求(也就是响应体中Location
的地址)。然而 Safari 浏览器在自动发起新的请求时,没有携带自定义的
Authorization
请求头,所以导致接口鉴权失败,返回了401
(Unauthorized
)。前端在收到接口响应后,由于响应体里面也返回了未登录的业务 code,就自动跳转到了登录页面。
这里还发现了一个有意思的细节:Safari 在发起重定向请求时,虽然没有带上
Authorization
请求头,但是会带上cookie
,这也说明了为什么在改造为 JWT 之前,Safari 能正常使用的原因(图中没有携带cookie
是因为 Demo 中没有set-cookie
)。然后我又在 Chrome 中进行了相同的测试,发现 Chrome 在发起重定向请求时,会携带
Authorization
请求头,所以能够正常使用。在 Chrome 中,具体请求如下(Chrome 中请求和重定向是 2 条独立的记录):
猜测可能
我当时的场景,后端返回的状态码是
301
,开始以为是各浏览器针对301
响应码的处理逻辑不一样。当时脑子里有个印象是:浏览器没有按照规范处理
301
和302
,所以后续规范新增了307
和308
。所以我想用 Charles 把请求的响应码改为
308
试试效果,搜了一些关于 Charles 修改 Status Code 的教程,找到了这个方法 is-it-possible-to-rewrite-a-status-code-with-charles-proxy,还有另一篇更为详细的教程 rewrite-modify-the-response,但是设置的流程比较繁琐。😫不过好在,发现了一个另外的网络调试软件 Proxyman,看到他之后,有一种春天来了的感觉。
使用起来非常方便,甚至可以直接通过 JavaScript 动态修改 Request / Response 的任何内容,这对于前端来说实在是太友好了,而且免费版就足以使用,强烈推荐大家下载体验一下 ~
可惜的是,通过 Proxyman 将请求的响应码改为
308
后,发现 Safari 依旧不会携带Authorization
请求头。😭搜索问题
既然猜想的方向不对,那就只能请教万能的 Google 了。
首先是在 stackoverflow 找到了这个问题 safari-does-not-persist-the-authorization-header-on-redirect,但是并没有解决。(在我写这篇文章的时候,发现 @sideshowbarker 已经给了最新回复:已在 Safari 15.4 修复)。
不过,提问者给出了自己项目的解决方案:最终改为了使用
cookie
来做身份验证。🤣后面又找到了另外一个相关问题 How to prevent Safari from dropping the Authorization header when following a same-origin redirect?,当时也是没有解决。
不过,我看到里面有一个评论说,他准备去 https://bugs.webkit.org/ 提 Bug,但是我在这个网站里面搜了一圈,没发现相关的问题。
所以,我就去注册了账号,新建了一个 Bug:Safari does not persist the Authorization header on redirect,并且在那个帖子里同步了一下:我已经创建过 Bug 了,后续可以在那个链接里面跟进(主要也是方便后续有其他小伙伴遇到这个问题,可以追踪后续进展)。
由于我的账号声望不足,没办法直接在问题中追加评论,所以只能新建一个回答。
楼主收到回答之后,说这个不是解决问题的方案,所以帮我把链接贴到了评论区,然后把那个回答给删了。
跟进处理
给 webkit 团队提完 Bug 之后,大概过了 2 周,官方回复说:他在技术预览版的 Safari中没有复现,并且给了一个他用来测试的 Demo,希望我也能够提供一个我这边复现的 Demo。
收到回复之后没多想,就立马着手准备代码。想着以最简单的方式复现,所以就用 Koa 来处理请求。
里面有一段逻辑是通过
ctx.headers
获取Authorization
的值,但是ctx.headers.Authorization
竟然是undefined
,后续将ctx.headers
对象打印出来才发现,里面所有的 key 都是小写的。开始以为是 Koa 自动把
request.header
中的 key 转为了小写,感到很疑惑。所以在 Koa 的 issues 中搜了一下,找到了这个问题,才知道这是 Node.js 的 http 模块做的处理。Node.js 将 header 的字段统一转为小写的原因是 rfc2616#section-4.2 中规定 header 字段是大小写不敏感的:
所以 Node.js 统一转为小写,避免使用方再重复做大小写转换的处理逻辑[参考]。
不过统一处理也存在一些不合理的场景,比如使用 Node.js 做 HTTP 代理服务时,转发后的请求头都自动变为小写了,那么会导致下层服务获取不到原始的请求头字段,这样会在传输的过程中破坏原始数据[参考]。
不过好在,Node.js 后续提供了新的 API,可以通过
req.rawHeaders
获取原始数据,具体可以看这个文档。关于 Node.js http 模块自动将 header 字段转为小写的详细讨论可以看这个链接。
P.S.:HTTP/2 的 rfc7540#section-8.1.2 规范,已明确规定 header 必须使用小写:
好了,弄明白了 header 小写的问题之后,就把准备好的 Demo 代码提交到了 GitHub。
完事之后,想着要不顺便部署一下吧,方便他们测试,我也可以趁这个机会体验一下 vercel。
不过部署之后,访问一直是 404,看了官方文档才发现,处理请求的文件,需要在
/api
目录中才行。可是我不太想修改文件目录,因为修改之后,访问的页面路径,也需要加上
/api
前缀。既然这样的话,那顺便试试 Next.js ?
没想到,代码写完之后,部署非常丝滑,一行命令直接搞定,而且给了对应的访问域名,重点是完全免费。
https://safari-redirect-demo.vercel.app/,这个是部署后生成的域名,二级域名是我 GitHub Demo 仓库的名称。
好了,Demo 准备完毕之后,就去回帖了,最终得出的结论是:这个 Bug 已经在 macOS 12.3 中修好了。
让我没想到的是,之前已经有人提过一个 Authorization header lost on 30x redirects 的类似 bug 了,可是我当初怎么没有搜到这个 😂。
另外,让我震惊的是外国友人也这么卷,快 23:00 了还在工作 🤔。
解决方案
现在来聊聊,在这整个过程中,我整理的 3 种解决方案。
升级版本(不靠谱)
目前 Safari 15.4(iOS 15.4, macOS 12.3) 已经修复了此问题,所以升级版本即可解决。
如果是公司内部系统,则可以根据实际情况来决定是否通过升级版本来解决此问题。
如果是对外项目,那这个方法肯定是没戏了,毕竟我们没办法控制用户升级系统。
存储到
cookie
(可行)在前面搜索的过程中,也有人通过把
token
放到cookie
中存储来解决这个问题的,因为 Safari 重定向时,虽然不会携带Authorization
,但是会把cookie
带上。但是这样需要后端配合,需要把鉴权的整个流程都改为从
cookie
中取值,这就要看你怎么说服后端大哥配合了。那么话又说回来了,既然要把
token
储存到cookie
,如果没有什么特殊场景的话,那可以直接考虑放弃 JWT 这套方案了 😝。手动处理(可行,Hack 思路)
Fetch
Fetch 有一个属性是
redirect
,它目前支持 3 个属性(摘自javascript.info:fetch-api#redirect):response.type="opaqueredirect"
和归零/空状态以及大多数其他属性。当时看到
manual
属性的时候,虽然描述看起来有点懵,但是想着可以手动处理重定向的请求,那肯定没毛病。当需要重定向时,我们从
header
中的location
中获取到新地址,然后手动对新地址发起一个请求,并且把Authorization
带上,这样总可以了吧 ~于是我开心的写了如下代码:
可是执行之后,并没有打印
newURL
,然后看了一下返回的response
对象,里面没啥有用的信息。status
的值是0
,headers
是个空对象感觉很奇怪,但是又看了看上面对
manual
的定义,好像明白了归零/空状态
是什么意思 😂。然后又开始了探索之路。
最终在 Fetch 规范仓库中搜到了一个 issue:Cannot get next URL for redirect="manual"。
这位同学和我有一样的疑惑(再次验证了那句话,你遇到过的 90% 的问题,其实别人都早已遇到了🤔):
manual
之后,获取不到redirect_url
。[参考]manual
换个名称更好?否则会引起误解。[参考]好了,没戏了,万万没想到,
manual
的意思不是手动处理,而是让浏览器不做处理 😳。难道这就是传说中的定义不规范,开发两行泪么 😭(这让我想起了请求头中
referer
字段拼写错误的问题,小声 BB 🤫)。不过好消息是,社区已经意识到这个问题,并且在讨论解决方案了,不过,这个问题从 2017 年被提出,到现在已经 5 年过去了,还没有标准落地,具体讨论可以查看此链接跟进。
既然 Fetch 无法获取到重定向的 URL,那 XMLHttpRequest 呢?
XMLHttpRequest
用 XMLHttpRequest 写了一个 Demo,发现浏览器也是会自动对重定向做出处理,打印的是重定向后最终的状态码,值为
200
,并不会打印307
,并且会获取到重定向后的返回值。然后通过搜索之后,找到这 2 个很有价值的问题:
总结来说,按照规定 XMLHttpRequest 在收到重定向请求时,会自动对新 URL 发起请求,并且规范中没有提供阻止重定向的方法。
但是可以通过
responseURL
属性获取到重定向的 URL:我试了一下,
responseURL
这个属性有 2 种取值逻辑,当本次请求:但是规范中提到,他有可能是空字符串:
所以,如果你一定要终止重定向请求,那么可以通过
responseURL
和原始的请求 URL 进行对比,如果不同,则表明存在重定向,但是不推荐使用这种逻辑判断,因为这不是官方标准。另外,这里的status
取到的是重定向后的值,所以不能用它对比。另外,通过这种逻辑进行重定向判断的,需要注意以下两点:
abort
终止重定向请求后,需要在onload
事件中做一层判断,因为 Safari 在请求终止后,还是会进入到onload
事件中。可通过status
进行判断,终止之后的请求,status
的值为0
。abort
终止重定向请求后,浏览器还是会对重定向的新 URL 发起请求,服务器也会正常处理并响应,所以需要注意此请求是否有「副作用」。到了这个时候就很有意思了,原来 XMLHttpRequest 不仅可以获取重定向的 URL,而且还可以通过
abort
终止重定向(不过并不推荐这种判断逻辑来终止请求)。好了,现在开始我们的 Hack 思路 ~
Hack 思路
先来说一下基本逻辑:
相关链接:
使用上述方法,虽然在 Safari 中可完美运行,但是控制台还是会打印
401
的错误,暂时还没有找到去除这个错误的方法,不过他并不会影响 JS 的运行逻辑,可暂时忽略。另外一个需要注意的点是:最好根据浏览器做一层判断,如果是 Safari,则将
redirect
设置为manual
,否则不进行处理。这样可以避免 Chrome 发起过多的无用请求(Chrome 总共会发出 5 个请求)。总结
这篇文章,前前后后总共写了 1 个多月,从最开始遇到这个问题,到 Safari 官方回复已在新版本中解决,再到写文章时梳理思路的整个过程,一直在刷新自己已有的认知,也使得这个过程变得非常有意思。
最开始遇到这个问题时,搜索了大量的资料,最终得出的结论是:可能是 Safari 的问题,只能等待官方解决,所以我给官方提了 Bug。
但是在搜索的过程中,我也发现了一些比较有意思的思路,所以就把那些链接记录了下来,准备空闲的时候整理一下。在这个过程中,其实大脑已经有了一个大概的解决方案和思路,就是通过
cookie
或者 Fetch 的redirect
属性解决。所以我在写这篇文章的时候,重点内容是 Fetch 解决方案,但是我在动手尝试的时候,发现
redirect
的manual
属性,不是手动处理的意思。然后又开始搜索「如何获取重定向的 URL」。最终发现 XMLHttpRequest 可以获取到,所以就有了最后的 Hack 思路,也算是画了一个完满的句号。
那么,我采用的是哪个方案呢?🤔
答案是,我没有选择上述的任何一个方案。因为我的场景只是单纯的把请求地址写错了,导致后端重定向到正确的地址。所以只需要把 URL 改一下即可。😝
收获
虽然整个过程非常的曲折漫长,但是这也让我意外的有了这些收获:
redirect=manual
配置,并不是手动处理重定向的意思,而是让浏览器不处理重定向。responseURL
获取到重定向的 URL。参考
The text was updated successfully, but these errors were encountered: