Closed (fixed)
Project:
Drupal core
Version:
8.3.x-dev
Component:
request processing system
Priority:
Major
Category:
Task
Assigned:
Unassigned
Reporter:
Created:
2 Jun 2016 at 05:13 UTC
Updated:
19 Jan 2017 at 14:04 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
dawehnerComment #3
wim leersWOAH what? :D
I'm not sure that it's a good idea to rely on PHP's typecasting behavior? :P
Impressive though!
Comment #5
Crell commentedRather than magic letters, it seems more generic and straightforward to use the 00. That is, on400() will catch any 4**, and then in parallel on500() would catch any 5**, etc. (Obviously in both cases presuming that a more specific version doesn't exist, in which case that gets used.)
Comment #6
dawehner@crell
I was thinking about the same but then I decided against that as it would not allow you to override the specific behaviour for exactly a 400, but if you think this is not a problem, using 400 is certainly much much better.
Comment #7
Crell commentedWell, 400 is a generic error. So would you want to differentiate at that point? Seems like it would probably be the same code; we can also verify that the actual number is available so on400() can branch internally if it really cares.
Comment #8
neclimdulXY is pretty confusing without reading the code. That said having 400 being a fallback might confuse people too. I kinda like the idea though... I think.
The
(string)in the cast seems repetitive since.is going to cast it anyways. Using (int) to truncate the decimal is clever though :).The only reason I can think that you would "care" that you're handling a different 4XX error would be because you want to bubble the original response code to the new response.
For example, this loose the 404 and 405 response codes on the response when we sets it to 403 here in the patch.
That might be solvable with a optional argument or something though. If we did that and someone absolutely had to handle 400 different from 4XX they could still do so by switching on the new argument.
Comment #9
dawehnerThank you for the feedback @neclimdul and @Crell
Good point!
Oh I totally forgot about that. Good catch!
As proven by the interdiff, this isn't an actual issue.
Comment #11
dawehnerWow, I would have not expected to get this wrong.
Comment #13
tstoecklerDon't want to hold this up, but I personally would consider
floor($code / 100) . '00'a bit more readable than the cast to int. Not sure what others think...Comment #14
dawehnerFair point. Its more explicit of what is going on. Thank you @tstoeckler
Comment #15
dawehnerHave a look at this interdiff, we also improve the exception message with this patch.
Comment #16
neclimdulYou could convert this to should array syntax if you wanted ;)
There are like 9k of changes to views and autocomplete that snuck in #11 that aren't clearly connected or in the interdiff. NW since I assume that was an accident?
Comment #17
dawehnerOh yeah totally, here is a new one.
Comment #18
neclimdulI really love the DX of this issue and think the kinks have been shaken out so going to be bold here :)
Comment #19
wim leersComment #21
neclimdulThere's a CI error in the log so I think this is still RTBC.
Comment #23
wim leersRandom fail.
Comment #24
effulgentsia commented400 is a specific status code, not a "anything between 400 and 499". In HEAD, we already have a
cache_ttl_4xxsetting and a4xx-responsecache tag, so what abouton4xx()methods?Agreed, but is
4xxconfusing, given we're already using that for the setting and cache tag?Comment #25
Crell commentedSee #7: A "generic" 4xx error and "400" error (which is a generic 4xx error by definition) would likely be the same, so this avoids duplicating those methods. If you don't want them to be you can still verify the error code itself in the handler method and branch with an if statement if you really need to.
Comment #26
dawehnerMaybe its really just me that 4xx has a slightly different meaning to 4xy :)
Comment #27
effulgentsia commentedWhat do you mean by "generic" in this case? A 400 error means "The request could not be understood by the server due to malformed syntax". As opposed to, for example, a 403 error, which means "The server understood the request, but is refusing to fulfill it.". I don't see how "could not be understood" is a generic version of "understood, but". Those are semantically disjoint.
So, 4xx is a generic way of referring to 400 and 403, but 400 is not a generic way of referring to 403.
Well, the key is that https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4 refers to it as 4xx, so that's what gives it meaning.
Sure. If a particular subscriber doesn't need on400() and on4xx() to be different, then it can implement the latter and not the former.
Comment #28
dawehnerThank you @effulgentsia for this great comment. It indeed seems to be the case that people in the computer industry simply start with 0 in their counting.
Comment #30
neclimdulThat pesky randomly failing update test. Looks like concerns are addressed, back to RTBC.
Comment #31
xjmRetitling per #27 also. I agree that 4XY might feel more familiar/accurate with algebraic notation, but I have never seen that before and so I keep thinking this issue is about a new JavaScript library or a men's body spray.
When @effulgentsia, @webchick, and I discussed this issue, we also wondered if this was too magical:
I'm still a bit... unsure about that, but if @effulgentsia thinks it is okay I won't block the issue.
Thanks @dawehner and @neclimdul. Leaving at RTBC for a framework manager.
Comment #32
xjmComment #33
wim leersNever was a big fan of "4xy", happy to see "4xx" now, especially since the spec apparently also uses this.
Comment #35
wim leersRandom fail.
Comment #36
xjmActually, we do need to add in-code documentation for this magical method API, explaining how to use it, and a CR for the removal of the statically named ones and the new ones. Thanks!
Comment #37
wim leersGood point.
The existing docs for the existing magic:
Now we need to document that you can also implement a catch-all
public function on4xx.Comment #38
neclimdulReally want to get this in so apologies for stealing the novice issues but here is the CR. https://www.drupal.org/node/2776671
Comment #39
dawehnerHa, well, some documentation in the code could be helpful as well ... :)
Comment #40
neclimdulgah, well I guess I'm sort of in a writing mood this morning so we are lucky. ;) Docs in the interdiff.
Comment #42
dawehnerLet's be consistent and use either
4xxor4XXComment #43
neclimdulYou're totally right sorry.
Comment #45
neclimdulAnd that's why we have tests. #2403307: RPC endpoints for user authentication: log in, check login status, log out added a on400 handler which merged cleanly but caused errors because the Symfony Response use was removed.
Comment #46
dawehnerAh nice! It is always good to see such examples of our test coverage
Comment #47
chx commented+ $method_fallback = 'on' . floor($exception->getStatusCode() / 100) . 'xx';Floating point math? http://i.imgur.com/14pDVr1.gifv
Comment #48
neclimdulHah, OK that's fair. Since strval adds a FCALL already, wouldn't substr() perform the same and be more readable though?
Comment #49
dawehnerNice trick to use strval!
Comment #50
chx commentedSame to me as long as you are not using floating point... sooner or later you will end up with the wrong method name when you don't expect it. Are you sure 400 / 100 is not 3.9999999999999999999999999995 deep inside?
Comment #51
neclimdulSounds good. There was one more "on4xx" handler added to JsonSubscriber I missed added by #2659070: REST requests without Content-Type header: unhelpful response significantly hinders DX, should receive a 415 response so here we go with one more update.
We might consider a follow up to use this in \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber which provides a big list of duplicated 4xx methods.
Comment #52
dawehnerWhatever works. NIce!
Comment #53
tstoecklerSorry for the spam, but it's very funny to me that we collectively figured out 4 different ways to go from e.g.
(int) 403to(string) '4'in this issue:(int) ($exception->getStatusCode() / 100)floor($exception->getStatusCode() / 100)strval($exception->getStatusCode())[0]substr($exception->getStatusCode(), 0, 1)I feel weirdly reminded of a first-semester computer science class. This is fun!
Comment #54
neclimdulOh, thanks to chx spurring the string approach I've got a few more if you're interested.
str_split($str)[0]if you don't like array syntax on strings.sprintf("%.1s", $exception->getStatusCode())if you like obscure c-ish methods of doing things.((string) $exception->getStatusCode())[0]If you're only using php7 this is the probably the ugliest and definitely the fastest since it avoids all FCALLs! :-DComment #55
chx commentedI nominate
as well. Bonus points to @neclimdul for sprintf. I haven't thought of that one.
Comment #56
chx commentedOh and if we wanted to keep floating point we could do
intval($exception->getStatusCode() / 100 + .0005)but we probably do not want to write obfuscated code...Comment #57
wim leers#55: NICE, I never knew
settype()existed!Comment #59
neclimdulCI broke? Its green now.
Comment #60
alexpottThis is only necessary to do if the method doesn't exist - so let's only work out the fallback if on404 (or whatever) does not exist.
Why is this change necessary?
Comment #61
alexpottSince the beta deadline has passed this has to go in 8.3.x now. Fortunately 8.3.x is open so this can get done soon.
Comment #62
dawehnerI disagree with that idea. We are microoptimizing here an edge case over much worse readability.
or
are both worse.
Well, what means necessary, but its the prove that this fallback works.
Before, this code was triggered:
\Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::onJsonso the final exception fallback for JSON. As you see, we are now actually sending an helpful message. A 401 is NOT a fatal.Given that I just re-RTBC-ed. Feel free to disagree :)
Comment #64
dawehnerLet's set it back to RTBC
Comment #65
wim leersWhy update only
\Drupal\Core\EventSubscriber\ExceptionJsonSubscriber?Why not also update
\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber?Then we update all
\Drupal\Core\EventSubscriber\HttpExceptionSubscriberBasesubclasses that do generic 4xx error handling.Comment #66
wim leersAlso, thanks for reviving this issue! I totally lost track of it. I hadn't forgotten, but couldn't find it anymore!
Comment #67
dawehnerGood point, let me try to do that. Note: This was the only remaining one which did all the HTTP methods. It doesn't make sense to handle a 429 for a HTML result I guess.
Comment #69
wim leersInvestigating.
Comment #70
wim leersTurns out RTBC'ing this at #64 (on December 12) didn't actually work, because the patch at #51 no longer applies to HEAD. Because
AuthTestno longer exists. Hence the DrupalCI error.If we look at that no-longer-applicable hunk, we see this:
If we compare that with the test failures of #67, we see this for
Drupal\Tests\hal\Functional\EntityResource\Block\BlockHalJsonBasicAuthTestand this for
Drupal\Tests\rest\Functional\EntityResource\Block\BlockJsonBasicAuthTest:The latter is exactly the same change as we had in
AuthTest. The former is new, because of the additional changes in\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriberadded in #67.So, let's revert the #67 interdiff for a moment, to prove that even the previous patch would've failed against HEAD (due to new test coverage).
Comment #71
wim leers#70 showed that even without the changes to
\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriberintroduced in #67, we have failing tests. Specifically, JSON + Basic Auth tests, like this one:This peculiar error response format is something I already noticed while working on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method. This is why I opened #2805281: ?_format=hal_json error responses are application/json, yet should be application/hal+json, to make all that consistent.
Root cause
The root cause is fairly simple: in HEAD,
ExceptionJsonSubscriberdoes not handle 401 HTTP exceptions. So it ends up using\Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::onJson(), whose logic does this:So that is where that peculiar prefix comes from!
Why does this patch change things?
This patch (including the one in #51) causes
\Drupal\Core\EventSubscriber\ExceptionJsonSubscriberto handle all 4xx HTTP exceptions, including 401. So, suddenly, it's not\Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::onJson()generating the error response, but\Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx(). And hence, the prefix disappears.So how to make the patch green?
I anticipated this in #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method. So I added a trait,
JsonBasicAuthWorkaroundFor2805281Trait, to ensure that all JSON + Basic Auth tests could easily and consistently be updated once we'd fix this in #2805281: ?_format=hal_json error responses are application/json, yet should be application/hal+json. Turns out we're fixing it here. That's fine too of course.This patch is relative to the one in #70.
Comment #74
wim leers#71 fixed all failing
GETtest coverage for all JSON + Basic Auth tests. But, now there's still failing tests in JSON test, for thePOSTandPATCHtest coverage.Unfortunately, this is actually blocked on #2813755: JSON responses encoded inconsistently: make them all RFC4627-compliant.
All remaining test failures in tests using the "JSON" format are failing like this:
In other words: inconsistent encoding problems. #2813755: JSON responses encoded inconsistently: make them all RFC4627-compliant makes them consistent, and causes these tests to pass. So, postponing this issue.
Comment #75
wim leers#2813755: JSON responses encoded inconsistently: make them all RFC4627-compliant landed, now re-uploading #71 so we can see how that affects the number of fails here.
Comment #77
wim leersInteresting, we now have 37 instead of 35 fails. That's more, not less.
But, as expected, all the encoding-related failures have disappeared. Investigating now.
Comment #78
wim leersApparently there are two random fails in
Drupal\views\Tests\Update\ArgumentPlaceholderUpdatePathTest. That explains the increase in number of failing tests. But really, it's the same: 35 in #71, 35 in #75.It's quite understandable the number of failures is the same: the REST-related tests now just are able to get further along in the test scenario until they fail.
Now investigating that.
Comment #79
wim leersFirst, we now observe a new interesting kind of failure in the test output:
500 responses instead of 422. That's bad.
The root cause is simple however: #2813755: JSON responses encoded inconsistently: make them all RFC4627-compliant added
\Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on422(), but this issue specifically wants to let\Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx()handle everything. Theon422()referenced a constant in a class that's no longer imported viause, so it results in a fatal error, and hence a 500 response.So, simply removing that
on422()method will fix that.Comment #81
wim leersAhhh, down to 10 fails! #79 finally proved #71 to be true: all JSON + Basic Auth tests are green now! And we did it simply be removing the temporary work-around trait.
Except there's this one JSON + Basic Auth test that is still failing:
RoleJsonBasicAuthTest. Apparently I forgot to use the trait there; it had the same logic as the trait but was never updated to use the trait. So, fixing that now.This will bring it down to 9 failures, all of which are HAL+JSON + Basic Auth tests.
Comment #82
wim leersWhat remains now: updating
\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber. Only then will have updated allHttpExceptionSubscriberBasesubclasses.This is largely the same interdiff as #67, except without the bug: the #67 interdiff converted all exceptions to a 400 response, this converts them to the appropriate 4xx response.
Comment #83
wim leers#82 will introduce zero extra failures. That's because all the REST test coverage only is testing the
jsonandhal_jsonformats — because thexmlformat is broken by design (it's being removed in #2800873: Add XML GET REST test coverage, work around XML encoder quirks).We can remove
ExceptionHalJsonSubscriber, so that thehal_jsonformat relies onDefaultExceptionSubscriberfor its exception-to-error-responses-handling. But doing so would change the response content type fromapplication/jsontoapplication/hal+json, which is an unrelated change and then also requires a bunch of unrelated changes in the test coverage. And actually, that's exactly what #2805281: ?_format=hal_json error responses are application/json, yet should be application/hal+json is determined to do. In fact, I've already added a patch at #2805281-10: ?_format=hal_json error responses are application/json, yet should be application/hal+json that applies cleanly after this is committed, which means this is now a blocker.So, in this issue, I'm not removing
ExceptionHalJsonSubscriber. The changes we made toExceptionJsonSubscriberin all above patches are sufficient (becauseExceptionHalJsonSubscriber extends ExceptionJsonSubscriber, and hence it inherited all changes). Hence this lets us remove the similar work-arounds for HAL+JSON + Basic Auth, just as described at the end of #71.Comment #87
wim leersFailures are all across Drupal core, known random fails: #2828559-55: UpdatePathTestBase tests randomly failing. Retesting, back to NR.
Comment #88
dawehnerFor me this looks really fine, maybe @damiankloip could give his meh as well?
Comment #89
damiankloip commentedYes, I like this a lot too. Being able to use xx method names as a kind of catch all is nice! Best of both worlds.
Comment #92
wim leersIt's green now!
Comment #93
dawehnerw00t, @damiankloip do you mind agreeing with it using a select list? Here is JS to execute, in case you are lazy:
jQuery('#edit-field-issue-status-und').val(14)Comment #94
damiankloip commentedHAHA, You know me too well. That snippet has been saved now :)
I do agree!
Comment #96
wim leersRANDOM FAIL STRIKES AGAIN.
Retesting…
Comment #98
wim leersComment #100
dawehnerRandom failures ...
Comment #102
wim leersComment #103
wim leersComment #104
alexpottCommitted efb8f71 and pushed to 8.3.x. Thanks!
Nice to get this done at last - the fails on #83 are all unrelated random fails.
Comment #106
wim leersWOOOT! This unblocked #2805281: ?_format=hal_json error responses are application/json, yet should be application/hal+json. (Which in turn is blocking #2827084: EntityNormalizer::denormalize should not throw UnexpectedValueException, but \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException, so we get a 422 response instead of 400.)
Comment #107
wim leersUpdated & published CR.