背景
上周有个vue项目, package.json中显示的vue-cli-service
版本是 "@vue/cli-service": "~4.5.0"
,本地开发代理配置突然不能使用了, 报308永久重定向错误

查看了一下vue.config.js中的代理转发配置,没发现明显问题,
js
devServer: {
proxy: {
'/api': {
target: 'https://test.xxx.com',
changeOrigin: true,
pathRewrite: {
'^/api': '',
},
},
},
},
又看了一下公共网络请求方法设置的基础路径, 没发现问题
js
const instance = axios.create({
// ...
baseURL:process.env.NODE_ENV == 'development' ? '/api' : env.VUE_APP_BASE_URL,
})
最后又查看了一下api定义处接口的url, 也看不出来问题。
js
// 店铺详情
storeInfo: (id) => {
return get(`/项目名/api-v2/ticketStore/noAuth/store/${id}`)
},
当时开发任务时间紧张,只能先迂回过去, 根据api文档定义,盲写业务逻辑, 写完之后,部署到线上环境去调试。今天相对空闲一些, 想查找一下引发问题的原因,毕竟本地开发,不可能不使用接口代理转发功能,问题迟早都要解决,绕不过去。
问题排查
以我过去排查疑难问题的经验, 一个比较有效的做法,就是把执行流程中的每个步骤的详细信息打印出来,问题一般就会水落石出。按照这个思路, 我给代理转发配置中添加了许多执行步骤日志, 看看转发的目标url是否正确,是从哪个环节开始不正常了。
在请求转发事件中,打印请求转发目标url
js
onProxyReq: (proxyReq, req, res) => {
console.log('=== 代理请求详情 ===')
console.log('URL:',`${proxyReq.protocol}//${proxyReq.getHeader('host')}${proxyReq.path}`)
}
在请求转发响应事件中, 打印响应数据
js
onProxyRes: (proxyRes, req, res) => {
console.log('=== 代理响应详情 ===')
console.log('状态码:', proxyRes.statusCode)
proxyRes.on('data', (chunk) => {
body += chunk.toString()
})
proxyRes.on('end', () => {
console.log('=== 错误详细信息 ===')
console.log(body)
})
},
重启服务,刷新了一下页面, 终端控制台输出如下:
js
=== 代理请求详情 ===
URL: https://test.xxx.com/xxx/api-v2/ticketStore/noAuth/store/578518674433958631?t=1756195546614
=== 代理响应详情 ===
状态码: 308
=== 错误详细信息 ===
<html>
<head><title>308 Permanent Redirect</title></head>
<body>
打印出来的目标url没问题, 但是响应不对。308和常见的301/302 重定向 类似, 表示目标地址已经迁移, 区别在于:308 要求客户端在重定向时必须保持原有的 HTTP 方法和请求体(比如 POST
还是 POST
,不能变成 GET
), 308 响应里通常会带一个 Location
头,指向新的地址, 那么这个新的地址是什么呢? 打印出来看看
vue-cli-service
比较坑爹, 不会像vite
一样, 每次修改了配置文件,重新启动服务,每次修改了vue.config.js的配置,都得手动重启服务,刷新页面。
js
onProxyRes: (proxyRes, req, res) => {
let body = ''
console.log('=== 代理响应详情 ===')
console.log('状态码:', proxyRes.statusCode)
// 打印所有响应头
console.log('响应头:', proxyRes.headers)
// 单独打印 Location
if (proxyRes.headers.location) {
console.log('Location:', proxyRes.headers.location)
}
}
打印出来的内容如下:很奇怪,这不是没转发之前的接口地址吗, 为什么要原路返回? 此刻心态平稳,头脑清晰的我发现, 协议头发生了变化, 为什么发出去的时候协议是http, 返回的响应中变成了https。
js
=== 代理响应详情 ===
状态码: 308
响应头: {
server: 'nginx/1.19.0',
date: 'Tue, 26 Aug 2025 08:22:09 GMT',
'content-type': 'text/html',
'content-length': '171',
connection: 'close',
location: 'https://localhost/xxx/api-v2/ticketStore/noAuth/store/578518674433958631?t=1756196529903',
'strict-transport-security': 'max-age=15724800; includeSubDomains',
'access-control-allow-origin': '*',
'access-control-allow-credentials': 'true',
'access-control-allow-methods': 'PUT, GET, POST, OPTIONS,DELETE',
'access-control-allow-headers': 'DNT,web-token,app-token,Authorization,Accept,Origin,Keep-Alive,User-Agent,X-Mx-ReqToken,X-Data-Type,X-Auth-Token,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,token,Cookie'
}
Location: https://localhost/xxx/api-v2/ticketStore/noAuth/store/578518674433958631?t=1756196529903
查了一下为什么协议会从 http
变成 https
的原因, 常见原因有:
-
后端应用 / 框架本身做了强制跳转
比如 Spring Boot、Django、Rails 等框架里,可以开启"强制 HTTPS",请求 HTTP 就自动 301/308 跳转到 HTTPS。
-
反向代理(Nginx/Traefik/Ingress)配置了 HTTPS 跳转
在代理里通常会有类似:
perlif ($scheme = http) { return 308 https://$host$request_uri; }
或者
kotlinreturn 308 https://$host$request_uri;
这会把所有 http 请求强制跳到 https。
-
应用识别到
X-Forwarded-Proto: http
和安全策略有的服务会检测请求头,如果发现是
http
协议,就直接重定向到https
。 (比如某些 API 网关、Kubernetes Ingress 默认行为) -
浏览器/客户端 HSTS 策略
如果你之前访问过
https://localhost
并且服务端设置了 HSTS 头,浏览器可能会强制所有后续请求走 HTTPS(不过你这里是服务端返回 308,更可能是前两种原因)。
逐条看了一下, 第三种情况的可能性最大。我先让运维查了一下最近Kubernetes Ingress有没有改动, 运维说最近半年都没有修改过,那么就得查请求头了,把请求头打印出来看一下
js
onProxyReq: (proxyReq, req, res) => {
// 追加打印
console.log('Headers:', proxyReq.getHeaders())
}
果然打印出来的请求头中有'x-forwarded-proto': 'http'
js
=== 代理请求详情 ===
// ...
Headers: [Object: null prototype] {
'x-forwarded-host': 'localhost:8080',
'x-forwarded-proto': 'http',
'x-forwarded-port': '8080',
'x-forwarded-for': '127.0.0.1',
为什么 x-forwarded-proto: http
会触发 308 重定向?
1. 后端/网关判断请求协议
- 大部分后端框架、API 网关、Ingress Controller(nginx-ingress、Traefik 等)都会根据
X-Forwarded-Proto
来识别"原始请求协议"。 - 如果它看到
x-forwarded-proto: http
,而站点要求强制 HTTPS,就会返回 301/302/308 跳转到https://
。
2. 典型 Nginx Ingress 配置
Nginx ingress 默认就有一个选项 force-ssl-redirect: true
。 它的逻辑就是:
js
if ($http_x_forwarded_proto = "http") {
return 308 https://$host$request_uri;
}
所以一旦代理传了 x-forwarded-proto: http
,Ingress 就会强制跳转。至此,问题已经水落石出。
修复问题
既然Nginx Ingress
遇到x-forwarded-proto: http
,就会执行308重定向, 那么只需在vue-service-cli
代理配置中, 每次转发请求时,移除x-forwarded-proto: http
请求头设置就可以了。
js
devServer: {
proxy: {
'/api': {
// 添加这句
xfwd: false,
}
}
}
果然,提交之后,打印的请求头中所有以x-forwarded-
开头的请求头都看不见了,代理响应也正常了。可是,为什么突然变成这样了,在没改项目配置的情况下。是不是@vue/cli-service
的版本最近有升级,查看了一下依赖链,发现与半年前相比,并无改变。
js
@vue/cli-service@4.5.19
↓
webpack-dev-server@3.11.3 (依赖 webpack@4.47.0)
↓
http-proxy-middleware@0.19.1
↓
http-proxy@1.18.1
↓
follow-redirects@1.0.0
我将项目的git版本进行了回退, 回退到半年前, 添加了请求头打印, 发现也会输出x-forwarded-
相关的请求头, 浏览器上显示请求响应代码是308, 所以这个问题不是前端的改动引起的,如果后端的话可信的话,可能是浏览器的安全策略升级导致的。
最后
我又找了一个vite项目对比了一下,发现vite.config.ts配置的server.proxy, 不会引发308重定向问题,打印了一下请求头,没有输出x-forwarded-xxx
, 那就奇怪了。难道vite的代理请求使用的不是http-proxy
, 查看了vite的官方源码,发现使用的代理工具果然不同, 是http-proxy-3
,它是对经典 http-proxy
的 TypeScript 重写版本。目标是解决原版 http-proxy
中的 socket 泄漏、安全漏洞和老旧 API。已用于生产环境。看了一下 http-proxy
最新的版本是v1.8.1
, 5年之前发布的, 现在还使用它的话,本地代理转发默认的配置会引发308重定向问题, 难怪vite不使用它了。通过对这个问题的排查,让我觉得,开发工具得与时俱进,不定期升级才行,否则就会出现莫名其妙的幺蛾子。