在 Chrome 中取消 Pre-Flight OPTIONS 请求(已经支持 CORS)

Posted

技术标签:

【中文标题】在 Chrome 中取消 Pre-Flight OPTIONS 请求(已经支持 CORS)【英文标题】:Pre-Flight OPTIONS requests are canceled in Chrome (CORS already supported) 【发布时间】:2019-07-13 15:47:00 【问题描述】:

我正在开发一个使用 Angular + Django(Django Rest Framework)的项目。在开发过程中,使用django-cors-headers、CORS_ORIGIN_ALLOW_ALL = TrueCORS_ALLOW_CREDENTIALS = True完成CORS支持。

当我尝试发送 POST 请求以在前端 (Angular) 中创建一些资源时,Chrome 会发送一些飞行前 OPTIONS 请求并由后端服务器 (python manage.py runserver) 成功响应,但其他请求则不是.这些请求由于未知原因被取消,后端服务器日志显示请求已被服务器接收并接受,详情如下图所示。

失败请求的标头如下所示。

但是,如果复制标头的内容并尝试使用curl 发送它,它会按预期工作。

$ curl -v -X OPTIONS -H "Access-Control-Request-Headers: authorization,content-type" -H "Access-Control-Request-Method: POST" -H "DNS: 1" -H "Origin: http://localhost:4200" -H "Referer: http://localhost:4200" -H "User-Agent: Mozilla/5.0" http:/localhost:8000/api/user-permissions/
* Unwillingly accepted illegal URL using 1 slash!
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 8000 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8000 (#0)
> OPTIONS /api/user-permissions/ HTTP/1.1
> Host: localhost:8000
> Accept: */*
> Access-Control-Request-Headers: authorization,content-type
> Access-Control-Request-Method: POST
> DNS: 1
> Origin: http://localhost:4200
> Referer: http://localhost:4200
> User-Agent: Mozilla/5.0
>
< HTTP/1.1 200 OK
< Date: Wed, 20 Feb 2019 02:47:39 GMT
< Server: WSGIServer/0.2 CPython/3.7.1
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Vary: Origin
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Origin: http://localhost:4200
< Access-Control-Allow-Headers: accept, accept-encoding, authorization, content-type, dnt, origin, user-agent, x-csrftoken, x-requested-with
< Access-Control-Allow-Methods: DELETE, GET, OPTIONS, PATCH, POST, PUT
< Access-Control-Max-Age: 86400
<
* Connection #0 to host localhost left intact

你知道这是怎么发生的吗?谢谢。


示例代码:

// The method of the component that invokes the methods of PermissionService.
  /** Update selected user's permissions. */
  updatePermissions() 
    const diff = this.diffPermissions();
    const toBeCreated = diff[0];
    const toBeDeleted = diff[1];
    this.isLoading = true;
    zip(
      this.permissionService.createUserPermissions(toBeCreated),
      this.permissionService.deleteUserPermissions(toBeDeleted),
    ).pipe(
      map(() => true),
      catchError((err: HttpErrorResponse) => 
        alert(err.message);
        return observableOf(false);
      ),
    ).subscribe(succeed => 
      this.isLoading = false;
    );
  

// The methods of PermissionService that issue the HTTP requests.

  createUserPermission(req: UserPermissionRequest) 
    return this.http.post(`$environment.API_URL/user-permissions/`, req);
  

  createUserPermissions(reqs: UserPermissionRequest[]) 
    // TODO(youchen): Evaluate the performance cost.
    return forkJoin(reqs.map(req => this.createUserPermission(req)));
  

  deleteUserPermission(permissionId: number) 
    return this.http.delete(`$environment.API_URL/user-permissions/$permissionId/`);
  

  deleteUserPermissions(permissionIds: number[]) 
    // TODO(youchen): Evaluate the performance cost.
    return forkJoin(permissionIds.map(id => this.deleteUserPermission(id)));
  

【问题讨论】:

请求标头部分中的“显示临时标头” 通常表明浏览器正在从其缓存中加载响应,而不是通过网络发送新请求。发生这种情况时,devtools 控制台中会记录什么错误消息?您能否在 Firefox 或任何其他浏览器中重现该故障? @sideshowbarker 我在 Chrome 和 Safari 的隐身模式下完成了这些操作。 devtools 控制台中不显示任何消息(当取消 OPTIONS 请求时)。但是,如果我在 Safari(隐身模式)中 Disable Cross-Origin restrictions,则不会发送任何 OPTIONS 请求,但我在 Django 日志中收到错误,ConnectionResetError: [Errno 54] Connection reset by peer。我很好奇为什么有些 OPTIONS 请求成功而有些则没有。 这些 OPTIONS 请求是否在 Safari 中也显示为已取消?如果 devtools 控制台没有显示错误,那么请求究竟以何种方式失败?你如何确定它失败了?如果有任何 CORS 问题或网络错误,浏览器将在 devtool 控制台中记录一些消息。因此,如果控制台中没有任何记录,这似乎表明浏览器已经确定它只是在做你的前端代码要求它做的事情。因此,您的前端代码中的某些内容可能导致这些请求被取消。但是你还没有展示你的代码,所以这里没有其他人能做的只是猜测 @sideshowbarker 是的,他们是。理想情况下,应该在 OPTIONS 请求完成后发送真正的 POST 请求,但在我的情况下,OPTIONS 请求被取消,之后没有发送任何 POST 请求。上面已经添加了示例代码,这些逻辑的入口是updatePermissions() 【参考方案1】:

找到原因:zip()不带参数

就我而言,我使用zip 来组合创建和删除,请参阅:

const createRequests = [c1, c2];
const deleteRequests = [d1, d2];

zip(
  this.service.create(createRequests),
  this.service.delete(deleteRequests),
)....

---
service.ts

create(reqs: CreateRequest[]) 
  return zip(...reqs.map(req => this.createSingle(req));


delete(reqs: DeleteRequest[]) 
  return zip(...reqs.map(req => this.deleteSingle(req));

但是如果createRequestsdeleteRequests 之一是一个空列表,这个逻辑就会出错。例如,如果createRequests 为空而deleteRequests 不为空,则所有由this.service.delete(deleteRequests) 触发的HTTP 请求都将被取消,因为zip() 返回一个空的zip()

解决方案:

这个问题的解决方案是我们检查reqs的长度并返回其他可观察到的实例。

固定代码:

create(reqs: CreateRequest[]) 
  if (reqs.length === 0) return of([]);
  return zip(...reqs.map(req => this.createSingle(req));


delete(reqs: DeleteRequest[]) 
  if (reqs.length === 0) return of([]);
  return zip(...reqs.map(req => this.deleteSingle(req));

【讨论】:

以上是关于在 Chrome 中取消 Pre-Flight OPTIONS 请求(已经支持 CORS)的主要内容,如果未能解决你的问题,请参考以下文章

如何让 Spring Security 使用 OAUTH2 响应 Pre-Flight CORS 请求

hapi.js Cors Pre-flight 不返回 Access-Control-Allow-Origin 标头

K8S安装node加入到节点错误Running pre-flight checks百分百解决,其他的都是片面的

chrome如何取消自动升级

无法在 chrome 上的移动视图中取消选中复选框输入 [重复]

Chrome 在下载 PDF 时发送两个请求(并取消其中一个)