Problem/Motivation
Over in #1869548: Opt-in CORS support we are trying to get support in for CORS requests. As a part of this, preflight OPTIONS requests are used. The current REST module doesn't support OPTIONS requests.
This is going to impact the ability of Drupal to be used with cross-domain ajax requests.
Proposed resolution
Add support for OPTIONS requests.
How to test
Run commands like the one below on your rest endpoints.
curl --request OPTIONS "http://drupal.d8/node/1?_format=json"
Remaining tasks
Implement #29.
User interface changes
None.
(Except an additional permission for >OPTIONS
per entity type.)
API changes
None.
http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-21#section-5.3.7
http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
Comment | File | Size | Author |
---|---|---|---|
#47 | interdiff.txt | 617 bytes | dawehner |
#47 | 2237231-47.patch | 7.95 KB | dawehner |
#42 | 2237231-41.patch | 7.89 KB | clemens.tolboom |
#36 | 2237231-36.patch | 7.85 KB | dawehner |
#26 | interdiff-2237231-23-26.txt | 1.44 KB | clemens.tolboom |
Comments
Comment #1
kim.pepperThis is an initial stab at adding OPTIONS support. No tests yet, and it assumes that if you can GET you can OPTIONS.
Reviews welcome!
Comment #2
Crell CreditAttribution: Crell commentedThis approach would only work for routes generated by REST module. And currently only for entities. That seems too narrow. We have all of the route data available. We should be able to just grab all the available routes at a path, check their legal methods, and return that. It should not be entity coupled.
That said, big +1 on supporting OPTIONS. It's just polite REST behavior.
Comment #3
dawehnerInteresting! Do you have some paper/reference you can link to?
At least this example in code clearly shows that we need some logic, regarding access for example. Do you suggest to put that directly on top of the routing layer?
I wonder whether we should also somehow expose that in views and just return GET ?
Comment #4
Crell CreditAttribution: Crell commentedI don't think OPTIONS is user-access-sensitive. It's only whether or not there is code to respond to a given method. My understanding is that it's a shorthand for "so what's going to give me a non-405". Not "what's going to give me a 200". So relying on entity access would actually be wrong anyway.
Comment #5
Grayside CreditAttribution: Grayside commentedI tried to find some supporting documentation for #4 but it's tricky to google. An argument in it's favor: CORS support brings in the "Access-Control-Max-Age" header as an option, which is a caching directive for OPTIONS responses (http://www.w3.org/TR/cors/#preflight-result-cache). Dynamic access control would probably mess with this cache.
Comment #6
kim.pepperAfter talking with Moshe and Crell at DrupalCon we decided the best approach is a request subscriber. Here's an initial patch which does all the route listening bit. I have no idea where to look for how to determine what methods we can access?? Appreciate any feedback.
Comment #8
kim.pepperI've added calls to the AccessManager, and works with manual tests locally. Need to add tests now.
Comment #10
kim.pepperHmmm. Turns out this is not working after all. Running through xdebug on a simple node/1 route shows that a method not allowed exception gets thrown. This is because there are no known routes that support an OPTIONS request.
Comment #11
jibranAdded links OPTIONS related docs.
Comment #12
kim.pepperI've added support for the OPTIONS response to the entity resource, and now I'm getting a 200 response for options requests. Not sure whether accessmanager is returning the right results, because all methods are being returned in the Allow header.
Comment #14
clemens.tolboomPatch needed a reroll.
Enabling the OPTIONS for content then testing
$ curl --user admin:admin --header 'Accept: application/hal+json' --request OPTIONS http://drupal.d8/node/1
gives HTML output with an 'access denied'. Hal module was disabled.gives an empty json.
I expected list of methods GET and authentication methods plus formats.
Comment #16
clemens.tolboomHmmm ... my result comes from an exception.
Event dispatching takes core/lib/Drupal/Core/Routing/AccessAwareRouter.php before this core/modules/rest/src/Routing/OptionsRequestSubscriber.php (rest.options_subscriber)
There is iirc an issue about event ordering.
Comment #17
clemens.tolboomEvent ordering is #2344645: The order of event subscribers should not be affected by definition order added to summary
Comment #18
clemens.tolboomReading #2344645: The order of event subscribers should not be affected by definition order is about priority and this patch has a priority of -10000 which is in weight terms first but on priority last.
Changed that number to 1000 which gives at least a debug step into onKernelRequest with
curl --verbose --header 'Accept: application/json' --request OPTIONS http://drupal.d8/node/1?XDEBUG_SESSION_START=19350
but the request is not complete.This result keeps empty so we need some tunig for the priority number
Comment #20
clemens.tolboomgives 9 values out of 12 listeners. The choosen '1000' in patch above is '200' and then '50' '35' '32' ... so what should it be ?!?
Comment #21
clemens.tolboomFix missing schema. How could I miss that :-/
Comment #22
dawehnerIt would be great to explain why we cannot implement this as ordinary route but need a request subscriber.
you can use isMethod() here.
I am confused, this property is not set anywhere.
Comment #23
clemens.tolboom#1 Added some documentation. Not sure whether that's enough. There is AFAIK no generic route as the OPTIONS methods matches any URI. See links to w3c in the summary.
#2 Done
#3 It's a class
protected $availableMethods = array(
New issue:
When debugging with ie
curl --verbose --header 'Accept: application/json' --request OPTIONS http://drupal.d8/node/1?XDEBUG_SESSION_START=16522
on a configured rest there still is no route found in
This needs a route expert.
Comment #24
dawehnerYeah this is somehow important information, maybe also link to the w3c page.
In this case use priority < 32
Comment #25
clemens.tolboom@dawehner why that magic < 32 ? It should be one of the highest values as OPTIONS must run before access check.
I failed to find the Access :: onKernelRequest implementation. Guess I'm searching wrong :-/
Comment #26
clemens.tolboomBased on the list below I guess we need to be @ 32+/- 0.5 . Note OPTIONS does not have to do an access / permission check.
Running
gives either an empty Allow header field or
{"error":"A fatal error occurred: No authentication credentials provided."}
depending on values above 33 (empty Allow) or 32 and below (error).
Adding authentication
gives an HTML exceptions response.
Recoverable fatal error: Argument 1 passed to Drupal\Core\Access\AccessManager::check() must implement interface Drupal\Core\Routing\RouteMatchInterface, instance of Symfony\Component\Routing\Route given, called in /Users/clemens/Sites/drupal/d8/www/core/modules/rest/src/Routing/OptionsRequestSubscriber.php on line 100 and defined in Drupal\Core\Access\AccessManager->check() (line 219 of core/lib/Drupal/Core/Access/AccessManager.php).
New patch
- adds puzzle to use the accept header but lacks to use this information. Mimetype is hardcoded to json. But that should use ie xml when requested. Not sure whether that is correct.
- lowers the priority from 1000 to 32 to see whether there are testbot failures too apart from mentioned exception.
Comment #27
clemens.tolboomStill learning about priorities.
$ grep -r -A 2 " tags:" . | grep priority
helps to find some but still no luck with level 35, 34 ... guess those are done through altering the prio?Open puzzle is how to tweak the priority so it runs before access check. See Remaining tasks
Comment #28
dawehnerThe answer is, you cannot anymore. Access checking is hardcoded inside
AccessAwareRouter
.Comment #29
Crell CreditAttribution: Crell commentedDaniel, Kalpana, and I discussed this at BADCamp. We need to support all routes, not just rest.module's routes.
The way to do that is to have a request listener that fires before routing (probably right before). If the method is not OPTIONS, do nothing. If it is, then it looks up available routes in the RouteProvider, does a union of all of their legal methods, and returns a response listing those. Routing then never happens, controllers never happen, etc.
The RouteProvider will still only be called once, EITHER by this new listener or by normal routing. Never both. So the performance impact should be negligible. (It's just one more request listener that is usually a no-op.)
Comment #30
AlxVallejo CreditAttribution: AlxVallejo as a volunteer and commentedCan we get a status update for this? My understanding is that CORS requires preflight negotiation with the OPTIONS method and that this is not natively a part of core or the CORS bundle. I'm surprised there's no Issues tab on the github repo for the CORS module either. https://github.com/previousnext/drupal_cors
CORS seems like mere necessity for decoupled applications and I've yet to resolve this in Drupal 8.
Comment #31
Wim LeersUpdated IS based on #29. This seems at least major, for the reasons outlined in #30.
Comment #32
Wim LeersComment #33
SlayJay CreditAttribution: SlayJay commentedAm I reading this right that drupal 8 doesn't support CORS requests? Thats... wow... That's a critical issue for the rest module isn't it?
Comment #34
Wim LeersCritical == "data loss, completely not working". CORS is not like that. The next priority level is "major". This issue is marked "major".
But I completely agree this is critical for REST. And that's also why this has the "rest-critical" issue tag. It's sad/unfortunate that D8's REST support doesn't yet have CORS. But this is open source, with people contributing in their free time.
If you'd like to help out with getting Drupal 8's REST to support CORS, that'd be great :) I'd be happy to provide reviews.
Comment #35
dawehnerLooking into #29 now, yeah!
Comment #36
dawehnerThere we go.
Comment #39
Wim LeersLooks great :)
Why "listener" and not subscriber"?
Should this also do access checking on all those routes? That probably doesn't make sense because additional metadata may need to be sent with the request for access to be granted.
Why 1000?
Nice :)
Comment #40
clemens.tolboomdawehner pointed out we allow GET + POST on almost any page which is done by #2019123: Use the same canonical URI paths as for HTML routes in \Drupal\Core\EventSubscriber\RouteMethodSubscriber::onRouteBuilding
[edit]
WimLeers foundRelated[/edit] #2505339: Stop using getMainRequest() to build $form['#action'] as the offender for always GET + POSTIt is weird to POST to a view until one realizes it could have filters etc. requiring a post.
I've tested a few router.pattern_outlines of which node/%, user/% fail on node/add, user/login aka string argument variants of nid or uid.
gives false results as one cannot DELETE, PATCH node/add or user/login. Similar goes for
I guess this patch is good to go but building an API scan based on OPTIONS result is futile.
Comment #41
dawehnerMH, I think we still need POST on most routes, otherwise people cannot just embed random forms in sidebars and what not.
I guess the reason for this strange behaviour is that
/node/add
is matched by/node/{node}
, as long you don't apply the regex.Comment #42
clemens.tolboom@WimLeers agreed : in #26 we had OptionsRequestListener (in a complete different namespace)
Should we anticipate in our tests for mentioned default POST result?
Attached patch contains the renaming.
Probably of topic aka ReST API related: I'm puzzled regarding _format. What if I configure POST only for hal+json? https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2 is unclear to me as they require a body when using 'Content-Type'
Comment #43
Wim LeersI didn't say #2505339: Stop using getMainRequest() to build $form['#action'] was the offender for GET+POST. That's a completely separate thing.
Comment #44
clemens.tolboom@Wim-leers: I've corrected in #40 ... sorry about that.
@dawehner I rather had the argument validated for a node/% rather then a match on node/add. But I guess we could RTBC this now right?
Comment #45
dawehnerRegarding the naming, I couldn't care less. A listener is for me more about the concrete function which gets called, while a subscriber also specifies what it subscribes to. The RouterListener just came next in the
core.services.yml
file.One remaining thing we could implement: Some form of configuration to enable options requests, but I guess given how fast they are responded there is no real a reason to make it toggleable.
@clemens.tolboom
I'm fine with some RTBC
Comment #46
Wim LeersRTBC as soon as
is answered by a code comment.Comment #47
dawehnerSure
Comment #48
Wim LeersAh, so that's the reason. Okay :)
Nit: s/its/it's/, can be fixed on commit.
Comment #49
clemens.tolboomIs this for 8.1.x or should it go for 8.2.x?
(+ a IS fix on curl command)
Comment #50
dawehnerWell, ideally it is for both, but I fear at least CORS is certainly a feature so we missed the 8.1.x deadline.
Comment #51
kim.pepperVery minor nitpick. (THe -> The)
Comment #52
mikl CreditAttribution: mikl at Högh Digital for Pfizer, Inc. commentedThis looks pretty much perfect, hope it gets in soon.
Comment #53
Crell CreditAttribution: Crell at Palantir.net for Acquia commentedI'm +1 here as well, with the two on-commit-able tweaks. Thanks all!
Comment #54
alexpottThis seems like a good candidate for 8.1.x-beta - it is new functionality this is isolated and should not break anything. Committed a4e5372 and pushed to 8.1.x and 8.2.x. Thanks!
Nits fixed on commit.