You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React客户端未回传HTTP-only Cookie致Express服务器401 Unauthorized问题

React + Express 401 Unauthorized 问题排查求助

React与Express项目中遇到API请求返回401 Unauthorized状态码的问题:服务器已配置HTTP-only Cookie和CORS,客户端能成功接收token,但调用需要认证的API时未将Cookie回传至服务器;无需token的API请求运行正常。已确认.env文件配置正确,Chrome开发者工具中启用了禁用缓存,并手动清除了浏览器中保存的HTTP-only Cookie,请求协助排查问题。


请求信息

Request URL: http://localhost:8000/api/listing/list/66325435f974e1389b48d311
Request Method: GET
Status Code: 401 Unauthorized
Remote Address: [::1]:8000 
Referrer Policy: strict-origin-when-cross-origin

### 响应头
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Content-Security-Policy: default-src 'self';script-src 'self' http://localhost:3000 'sha256-hash-of-script-content';object-src 'none';upgrade-insecure-requests;base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;script-src-attr 'none';style-src 'self' https: 'unsafe-inline'
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: cross-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin, Accept-Encoding
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
Content-Type: application/json; charset=utf-8
ETag: W/"72f-Cf8NP8RMMXaFfkEWgmAlCOlVVQo"
Content-Encoding: gzip
Date: Sun, 01 Sep 2024 08:37:03 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

### 请求头
GET /api/listing/list/66325435f974e1389b48d311 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9,de;q=0.8
Cache-Control: no-cache
Connection: keep-alive
Content-Security-Policy: default-src 'self'; connect-src 'self' http://localhost:8000;
Content-Type: application/json
Host: localhost:8000
Origin: http://localhost:3000
Pragma: no-cache
Referer: http://localhost:3000/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
sec-ch-ua: "Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

客户端代码

请求头配置

const __baseURL = import.meta.env.VITE_APP_API_ENDPOINT;

export const fetchHeaders: Record<string, string> = {
    "Content-Type": "application/json",
    "X-XSS-Protection": "1; mode=block",
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY",
    "Content-Security-Policy": `default-src 'self'; connect-src 'self' ${__baseURL};`
};

API请求配置

const options: RequestInit = {
    method: httpMethod.toUpperCase(),
    headers: fetchHeaders,
    credentials: 'include' // for CORS
};

完整API管理代码

import xss from "xss";

const __baseURL = import.meta.env.VITE_APP_API_ENDPOINT;

export const fetchHeaders: Record<string, string> = {
    "Content-Type": "application/json",
    "X-XSS-Protection": "1; mode=block",
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY",
    "Content-Security-Policy": `default-src 'self'; connect-src 'self' ${__baseURL};`
};

type TApi = {
    urlPath: string;
    httpMethod: 'get' | 'post' | 'delete' | 'put';
    apiParam?: object | string;
};

export async function apiManager({ httpMethod, urlPath, apiParam }: TApi): Promise<{ data: any, error: string | null, status: number }> {
    try {
        let fullUrl = `${__baseURL}${urlPath}`;
        

        const options: RequestInit = {
            method: httpMethod.toUpperCase(),
            headers: fetchHeaders,
            credentials: 'include' // for CORS
        };

        if ((httpMethod === 'get' || httpMethod === 'delete') && apiParam) {
            if (typeof apiParam === 'object') {
                const queryString = new URLSearchParams(apiParam as Record<string, string>).toString();
                fullUrl = `${fullUrl}?${xss(queryString)}`;
            } else if (typeof apiParam === 'string') {
                fullUrl = `${fullUrl}?${xss(apiParam)}`;
            }
        } else if ((httpMethod === 'post' || httpMethod === 'put') && apiParam) {
            options.body = xss((typeof apiParam === 'object') ? JSON.stringify(apiParam) : apiParam);
        }

        console.log('fullUrl:', fullUrl)
        console.log('options:', options)

        const res = await fetch(fullUrl, options);
        const status = res.status;

        let data: any = null;
        let error: string | null = null;

        try {
            data = await res.json();
        } catch (jsonError) {
            error = `Failed to parse response as JSON. Status: ${status}`;
        }

        if (!res.ok) {
            switch (status) {
                case 400:
                    error = 'Bad Request. Please check your input and try again.';
                    break;
                case 401:
                    error = 'Unauthorized. Please log in to access this resource.';
                    break;
                case 403:
                    error = 'Forbidden. You do not have permission to access this resource.';
                    break;
                case 404:
                    error = 'Not Found. The resource you are looking for might have been removed.';
                    break;
                case 409:
                    error = 'Conflict. There was a conflict with the current state of the resource.';
                    break;
                case 422:
                    error = 'Unprocessable Entity. Validation errors occurred.';
                    break;
                case 500:
                    error = 'Internal Server Error. Please try again later.';
                    break;
                case 502:
                    error = 'Bad Gateway. Received an invalid response from the upstream server.';
                    break;
                case 503:
                    error = 'Service Unavailable. The server is currently unable to handle the request.';
                    break;
                case 504:
                    error = 'Gateway Timeout. The server did not receive a timely response from the upstream server.';
                    break;
                default:
                    error = `HTTP error! Status: ${status}`;
                    break;
            }

            if (data.message)
                error += '\n- ' + data.message
            
        }

        return { data, error, status };

    } catch (networkError: unknown) {
        console.error('Network/API error:', networkError);

        let errorMessage = 'An unknown error occurred';
        if (networkError instanceof Error) {
            errorMessage = networkError.message;
        }

        return { data: null, error: errorMessage, status: 0 };
    }
}

API调用示例

const { data, error } = await apiManager({
  urlPath: '/api/listing/list/' + currentUser?._id,
  httpMethod: 'get',
});

if (data?.success === false || error) {
  toast.error(error, toastBody)
  return
}

服务器代码

import cors from "cors";

const corsOptions = {
  origin: envManager.ORIGIN,
  credentials: true,
  optionsSuccessStatus: 200
};
app.use(cors(corsOptions));

app.use((req, res, next) => {
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin-allow-popups');
  res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
  res.setHeader('Referrer-Policy', 'no-referrer-when-downgrade');
  next();
});

// 登录控制器响应
res
  .cookie(__SERVER_ACCESS_TOKEN, token, {
    httpOnly: true,
    secure: process.env?.NODE_ENV === 'production',
    sameSite: 'none',
    path: '/',
    domain: envManager.ORIGIN,
  })
  .status(200)
  .json(rest);

核心排查点及修复建议

  • Cookie Domain配置错误:登录接口中domain: envManager.ORIGIN如果是http://localhost:3000,属于无效配置——domain只能是主机名(如localhost),不能带协议和端口。浏览器会拒绝绑定此类Cookie,导致后续请求无法携带。
    修复:将domain改为localhost,或在开发环境下直接省略该字段:
    res.cookie(__SERVER_ACCESS_TOKEN, token, {
      httpOnly: true,
      secure: process.env?.NODE_ENV === 'production',
      sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
      path: '/',
      // domain: envManager.ORIGIN, // 注释或改为'localhost'
    })
    
  • SameSite与Secure冲突:sameSite: 'none'要求必须配合secure: true(HTTPS环境),开发环境下secure为false,会导致Cookie被浏览器标记为无效。开发环境建议将sameSite改为'lax'或'strict'。
  • 移除客户端冗余请求头:客户端fetchHeaders中的Content-Security-Policy是响应头字段,不需要在请求中发送,建议移除,避免干扰浏览器策略。
  • 验证Cookie存储状态:在Chrome开发者工具Application -> Cookies -> http://localhost:8000下,确认Cookie是否存在,检查domain、path、httpOnly等属性是否符合预期。

内容的提问来源于stack exchange,提问作者user769371

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 23:59:55