Yii2: Regression: Cors::beforeAction()

Created on 18 Feb 2018  ·  53Comments  ·  Source: yiisoft/yii2

There's a regression in 2.0.14-dev Cors::beforeAction() after merge: https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d

This is my test in Codeception:

public function preflight(ApiTester $I): void
{
    $I->haveHttpHeader('Access-Control-Request-Headers', 'Content-Type');
    $I->haveHttpHeader('Access-Control-Request-Method', 'POST');
    $I->haveHttpHeader('Accept', '*/*');
    $I->sendOPTIONS('/users');

    $I->seeResponseCodeIs(200);
    $I->seeHttpHeader('Content-Type', 'application/vnd.api+json; charset=UTF-8');
    $I->seeHttpHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
    $I->seeHttpHeader('Access-Control-Allow-Headers', 'Content-Type');
}

Before merge https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d:

See http header "Content-Type","application/vnd.api+json; charset=UTF-8"

- Expected | + Actual
@@ @@
-'application/vnd.api+json; charset=UTF-8'
+'application/vnd.api+json; charset=UTF-8'

After merge https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d:

See http header "Content-Type","application/vnd.api+json; charset=UTF-8"

- Expected | + Actual
@@ @@
-'application/vnd.api+json; charset=UTF-8'
+'text/html; charset=UTF-8'

Additional info

| Q | A
| ---------------- | ---
| Yii version | 2.0.14-dev
| PHP version | 7.1.4
| Operating system | macOS High Sierra 10.13.3

under discussion

Most helpful comment

If so, moving to 2.0.15 to think if it can be done better.

All 53 comments

What's the actual code that is tested?

@samdark, this is an action that I testing:

public function actionCreate()
{
    $resource = new SignUpResource();

    if ($resource->load($this->request->post()) && $resource->validate()) {
        try {
            $this->service->create($resource);
            $this->response->setStatusCode(201);
            $this->response->getHeaders()->set('Location', Url::to('users/' . $resource->id, true));

            return $resource;
        } catch (\DomainException $e) {
            \Yii::$app->errorHandler->logException($e);
        }
    } elseif ($resource->hasErrors()) {
        return $resource;
    }
}

rest controller:

class BaseController extends \yii\web\Controller
{
...
    public $serializer = \tuyakhov\jsonapi\Serializer::class;

    public $enableCsrfValidation = false;

    public function behaviors(): array
    {
        return [
            'contentNegotiator' => [
                'class' => \yii\filters\ContentNegotiator::class,
                'formats' => [
                    'application/vnd.api+json' => \yii\web\Response::FORMAT_JSON,
                    'application/json' => \yii\web\Response::FORMAT_JSON,
                    'application/xml' => \yii\web\Response::FORMAT_XML,
                ],
            ],
            'verbFilter' => [
                'class' => \yii\filters\VerbFilter::class,
                'actions' => $this->verbs(),
            ],
            'rateLimiter' => [
                'class' => \yii\filters\RateLimiter::class,
            ],
        ];
    }

config:

'components' => [
    'request' => [
        'parsers' => [
            'application/vnd.api+json' => [
                'class' => tuyakhov\jsonapi\JsonApiParser::class,
                'memberNameCallback' => [tuyakhov\jsonapi\Inflector::class, 'variablize'],
            ],
            'application/json' => yii\web\JsonParser::class,
        ],
    ],
    'response' => [
        'formatters' => [
            yii\web\Response::FORMAT_JSON => [
                'class' => tuyakhov\jsonapi\JsonApiResponseFormatter::class,
                'prettyPrint' => YII_DEBUG,
                'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
            ],
        ],
    ],
    'urlManager' => [
        'enablePrettyUrl' => true,
        'enableStrictParsing' => true,
        'showScriptName' => false,
        'rules' => [
            ...
            'OPTIONS,GET users' => 'users/index',
            'OPTIONS,POST users' => 'users/create',
        ],
    ],
],
'as corsFilter' => [
    'class' => yii\filters\Cors::class,
],

It's not clear how to reproduce it. There are custom classes everywhere and excessive config. Would you please do a minimal test case that contains only necessary code to reproduce the issue starting with basic app?

@samdark, I'll try.

@samdark, the issue is reproduce in the basic app cors-bug.zip.

How to reproduce the issue.

  1. composer install
  2. php vendor/bin/codecept run api
    The result should be fail.
  3. These lines should be comment out. https://github.com/yiisoft/yii2/blob/4f80cda7130da5259ce93fe41eb681cbb70f30be/framework/filters/Cors.php#L109-L113
    The result should be success.

This is strange bacause Cors filter is not enabled in this project

@leandrogehlen and you confirm that commenting lines above make tests pass?

I'm looking for it now

I did some tests and the unique issue is that if you comment second line the test works.
Then, i can't see the problem, because the header: 'Content-Type' is not set in request and ContentNegotiator not add this header in response. I think this is right.

```php
$I->seeResponseCodeIs(200);
//$I->seeHttpHeader('Content-Type', 'application/json; charset=UTF-8');
$I->seeHttpHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
$I->seeHttpHeader('Access-Control-Allow-Headers', 'Content-Type');

I think i found the problem, in example project was configured in web.php:

   'as corsFilter' => [
        'class' => yii\filters\Cors::class,
    ],
   'as authenticator' => [
        'class' => yii\filters\auth\CompositeAuth::class,
        'except' => ['site/index', 'debug/*'],
        'authMethods' => [
            ['class' => yii\filters\auth\HttpBearerAuth::class],
        ],
    ],

This way behavior method is not considered. I removed this configuration and did it in controller :

class SiteController extends Controller
{
    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            'contentNegotiator' => [
                'class' => ContentNegotiator::class,
                'formats' => [
                    'application/json' => Response::FORMAT_JSON,
                ],
            ],
            'cors' => [
                'class' => Cors::class,
            ]
        ]
   ...

I change the test to

    $I->sendOPTIONS('/site/index?_format=json');

This worked, but i think the problem is the sequence, like cors return false the following filters are not executed

Well, that's expected and exactly that was changed, isn't it?

This worked, but i think the problem is the sequence, like cors return false the following filters are not executed

This is the correct statement.

@samdark, i think so.
@alexraputa if you change the filters sequence your problem is solved?

@leandrogehlen, no. If I change the filters sequence my authorization is failing. But the preflight test is passed.

@alexraputa why do you need these headers or authentication in preflight?

@leandrogehlen, this is definitely a bug.
The preflight response should contain Content-Type, which will be the same as for a real response.
See CORS the section of Preflighted requests and example for it.

I agreee, but this is happening. You don't adds the 'Content-Type' in your request test.

If you use the code bellow, will work

$I->haveHttpHeader('Content-Type', 'application/json; charset=UTF-8');
$I->haveHttpHeader('Access-Control-Request-Headers', 'Content-Type');
$I->haveHttpHeader('Access-Control-Request-Method', 'POST');
$I->haveHttpHeader('Accept', '*/*');
$I->sendOPTIONS('/users');

@leandrogehlen, I disagree with it. The Content-Type of the request specifies the request body, which is not applicable to an OPTIONS request.
See OPTIONS request.

I use behaviors in this sequence. See Yii 2 CORS.
2018-02-20 19 38 05

The behaviors will not work if the first of them return false in the beforeAction method.

@samdark, I think we should revert this https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature, until we find a better solution.

@alexraputa, I understand but i belive the interruption in filter sequence is another issue, not associate to Cors. If it will considered a bug is a problem for all filters

@samdark, I can't see problem, does not make sense execute others filters in preflight, because after preflight the real request will be executed

@leandrogehlen, @samdark, Such frameworks as Laravel and Symfony does not stop the application in the cors filter if the request has OPTIONS method. They simply modify the response object and pass it further along the chain of middlewares. Why not do the same in Yii? We should not leave this as is.

@alexraputa, isn't to OPTIONS method is only in preflight:
https://github.com/yiisoft/yii2/blob/master/framework/filters/Cors.php#L109

related #14618

@leandrogehlen, The preflight request is equal to OPTIONS request.

Unlike “simple requests”, "preflighted" requests first send an HTTP request by the OPTIONS method to the resource on the other domain, in order to determine whether the actual request is safe to send. Cross-site requests are preflighted like this since they may have implications to user data.

Example of a request from documentation (Preflighted requests):

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

Example of a response from documentation (Preflighted requests):

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

Example of a real request from documentation (Preflighted requests):

POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache

<?xml version="1.0"?><person><name>Arun</name></person>

Example of a real response from documentation (Preflighted requests):

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]

Both responses have the same content type - Content-Type: text/plain.

@samdark, need your thoughts.

This https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature also breaks BC if you have actionOptions like this:

public function actionOptions(): void
{
    if (!$this->request->getIsOptions()) {
        $this->response->setStatusCode(405);
    }

    $options = implode(', ', array_reduce($this->verbs(), 'array_merge', []));
    $this->response->getHeaders()->set('Allow', $options);
    $this->response->getHeaders()->set('Access-Control-Allow-Method', $options);
}

Because the response was stopped in the beforeAction() method of the CORS filter. And your response will not contain the Allow and the modified Access-Control-Allow-Method headers for specific actions of controllers.

@alexraputa according to CORS spec https://www.w3.org/TR/cors/#preflight-request the only considered headers are CORS-related. The rest of the headers and body are simply ignored so it doesn't make any sense to send them.

Why would you need to send Allow or Content-type if they're simply ignored?

Why would you need to send Allow or Content-type if they're simply ignored?

@samdark, This is not true. The response from the server must support these headers. See rfc7231 and OPTIONS
W/ this https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature.
My application on Ember.js 3.0 sends a preflight request:

request

And backend app on Yii 2 sends response:

response

W/o this https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature.

response2

This is a standard preflight request to the server. The other frontend frameworks such as React and Angular send the same preflight request.
If frontend app send the Accept: */* (see Accept) header then backend app must send the allowed Content-Type for a URL.

Well, this is not how preflight should work according to its specification: https://www.w3.org/TR/cors/#preflight-request

Do you have any actual misbehavior caused in your app because of the change?

My API is public and clients are using it. If the Accept header receives the response with Content-Type liketext/html, then clients accordingly need to implement a serializer for this type of content. But then the API sends a real response with the type application/vnd.api+json and the frontend application throws an error, because the serializer for application/vnd.api+json is not implemented.

@alexraputa, this is not true, i have built an angular application with yii2 cors filters and it worked fine.

Preflight results:
preflight

Real request:
real-request

Yii2 Angular Project
yii2-angular.zip

cd angular
npm install
ng build --watch

to access angular app use: http://localhost/yii2-angular/web/dist/

@leandrogehlen, I don't see any of CORS methods in your response.
The preflight request shouldn't have the Content-Type header because it has an empty body.
But you force added the Content-Type header into the request.

let headers = new HttpHeaders({
  'Content-Type': 'application/json'
});

@samdark, please close this issue, he wants to have a response default format.
it has nothing to do with issue.

Ok, if you want to have some rare bugs in a framework then just leave it.

@alexraputa which client do you have that doesn't follow CORS specification and looks at non-CORS headers in preflight?

@samdark, Which the headers of the my preflight request do not match with the CORS specification?

  • Accept;
  • Referer;
  • Access-Control-Request-Headers;
  • Origin;
  • User-Agent;
  • Access-Control-Request-Method.

Not the CORS specification itself but preflight part of it will just ignore what's not needed for CORS:

  • Exclude the author request headers.
  • Exclude user credentials.
  • Exclude the request entity body.

What matters (and is cached in browsers) in CORS reponse is:

  • origin
  • url
  • max-age
  • credentials
  • method

That's it. Nothing about content-type, accept or anything else. Body doesn't matter as well.

So I'm asking again, what's the client that you're using that needs Accept header in the response for preflight request?

@samdark,

My application on Ember.js 3.0 sends a preflight request:

These are preflight requests specification in ASP.NET (Preflight-requests):

  • The application doesn't set any request headers other than Accept, Accept-Language, Content-Language, Content-Type, or Last-Event-ID, and

  • The Content-Type header (if set) is one of the following:
    application/x-www-form-urlencoded
    multipart/form-data
    text/plain

You can close this issue If you think that this is not a bug.

You're confusing request and response. See response example there:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 0
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT
Date: Wed, 20 May 2015 06:33:22 GMT

According to W3C spec, Cache-Control: no-cache, Pragma: no-cache and Date are ignored and aren't cached on clients so it comes to:

HTTP/1.1 200 OK
Content-Length: 0
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT

What exactly happened to Ember.js 3.0 app when we started to send these reduced CORS responses? Did it break somehow? How exactly?

These are preflight requests specification in ASP.NET (Preflight-requests)

@alexraputa according to that linked documentation those are not specifications but conditions that when meet the browser will omit or skip the process of sending a preflight request.

Those are conditions similar to what's on W3C except examples are a bit weird. They're OK but, as I said, they define request, not response.

@samdark, what do you think about it Preflighted requests (see example of preflight request/response)?
And about this https://github.com/yiisoft/yii2/issues/15665#issuecomment-367489876.
We already have the actionOptions method in the ActiveController class. The headers of this action are never included in the response if you use the CORS filter.

https://github.com/yiisoft/yii2/blob/77ad6bc00847d4964a0b2a82d3b70dcd7cb5a1cf/framework/rest/ActiveController.php#L102-L104
https://github.com/yiisoft/yii2/blob/77ad6bc00847d4964a0b2a82d3b70dcd7cb5a1cf/framework/rest/OptionsAction.php#L32-L44

What exactly happened to Ember.js 3.0 app when we started to send these reduced CORS responses? Did it break somehow? How exactly?

The front-end application is working. But the response from the API was significantly reduced, which does not give a full submission of the resource.

I think that these response examples are OK as well but, as pointed out by specification (W3C one), non-CORS headers are not taken into account. Means you can send whatever you want: any headers, body... anything. As long as response code is 2xx and CORS headers are there, it's fine. But these non-CSRF headers doesn't matter.

About https://github.com/yiisoft/yii2/issues/15665#issuecomment-367489876. Regular OPTIONS request and preflight request are two different things. There's no need to give out usual options response in CORS preflight response.

Or did you mean that regular OPTIONS response (non-preflight one) is broken?

The front-end application is working. But the response from the API was significantly reduced, which does not give a full submission of the resource.

Would you please elaborate "does not give a full submission of the resource"?

@samdark, https://github.com/yiisoft/yii2/issues/15665#issuecomment-367602078.

You can close this issue If you think that this is not a bug.

I don't get it :( Does this change affect non-preflight responses?

No, it's not affect. But it's confusing.

I don't modify any response on API, but framework send the Content-Type: text/html.

preflight-requests
preflight-request

I see. So there's no error per se and it's overall just confusing, right?

If so, moving to 2.0.15 to think if it can be done better.

I see. So there's no error per se and it's overall just confusing, right?

Yes, it's right.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

samdark picture samdark  ·  63Comments

alexandernst picture alexandernst  ·  163Comments

sepidemahmoodi picture sepidemahmoodi  ·  104Comments

vercotux picture vercotux  ·  47Comments

njasm picture njasm  ·  44Comments