Hi, folks look like I having problems since #3038308: Avoid translations DELETE data loss and unintended changes with PATCH and test all methods against entity route parameter translation upcasting.
I can't PATCH
my custom content entity since I updated module 2.3.0 => 2.4.0.
I have a custom content entity type, which is not translatable. The site is installed on different from the English language.
For example I send this request:
{
"data":{
"attributes":{
"due_date":"2019-03-02"
},
"id":"d38dc101-4c91-4522-a0c6-56e86a660802",
"type":"scrm-issue--scrm-issue"
}
}
And got this response:
{
"jsonapi":{
"version":"1.0",
"meta":{
"links":{
"self":{
"href":"http:\/\/jsonapi.org\/format\/1.0\/"
}
}
}
},
"errors":[
{
"title":"Method Not Allowed",
"status":"405",
"detail":"The requested translation of the resource object does not exist, instead modify one of the translations that do exist: und.",
"links":{
"via":{
"href":"http:\/\/scrm.localhost\/jsonapi\/scrm_issue\/scrm_issue\/d38dc101-4c91-4522-a0c6-56e86a660802"
},
"info":{
"href":"http:\/\/www.w3.org\/Protocols\/rfc2616\/rfc2616-sec10.html#sec10.4.6"
}
},
"source":{
"file":"\/var\/www\/html\/web\/modules\/contrib\/jsonapi\/src\/ParamConverter\/EntityUuidConverter.php",
"line":78
},
"meta":{
"exception":"Symfony\\Component\\HttpKernel\\Exception\\MethodNotAllowedHttpException: The requested translation of the resource object does not exist, instead modify one of the translations that do exist: und. in \/var\/www\/html\/web\/modules\/contrib\/jsonapi\/src\/ParamConverter\/EntityUuidConverter.php:78\nStack trace:\n#0 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/ParamConverter\/ParamConverterManager.php(100): Drupal\\jsonapi\\ParamConverter\\EntityUuidConverter-\u003Econvert(\u0027d38dc101-4c91-4...\u0027, Array, \u0027entity\u0027, Array)\n#1 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/Routing\/Enhancer\/ParamConversionEnhancer.php(45): Drupal\\Core\\ParamConverter\\ParamConverterManager-\u003Econvert(Array)\n#2 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/Routing\/Router.php(260): Drupal\\Core\\Routing\\Enhancer\\ParamConversionEnhancer-\u003Eenhance(Array, Object(Symfony\\Component\\HttpFoundation\\Request))\n#3 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/Routing\/Router.php(131): Drupal\\Core\\Routing\\Router-\u003EapplyRouteEnhancers(Array, Object(Symfony\\Component\\HttpFoundation\\Request))\n#4 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/Routing\/AccessAwareRouter.php(92): Drupal\\Core\\Routing\\Router-\u003EmatchRequest(Object(Symfony\\Component\\HttpFoundation\\Request))\n#5 \/var\/www\/html\/vendor\/symfony\/http-kernel\/EventListener\/RouterListener.php(115): Drupal\\Core\\Routing\\AccessAwareRouter-\u003EmatchRequest(Object(Symfony\\Component\\HttpFoundation\\Request))\n#6 [internal function]: Symfony\\Component\\HttpKernel\\EventListener\\RouterListener-\u003EonKernelRequest(Object(Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent), \u0027kernel.request\u0027, Object(Drupal\\Component\\EventDispatcher\\ContainerAwareEventDispatcher))\n#7 \/var\/www\/html\/web\/core\/lib\/Drupal\/Component\/EventDispatcher\/ContainerAwareEventDispatcher.php(111): call_user_func(Array, Object(Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent), \u0027kernel.request\u0027, Object(Drupal\\Component\\EventDispatcher\\ContainerAwareEventDispatcher))\n#8 \/var\/www\/html\/vendor\/symfony\/http-kernel\/HttpKernel.php(127): Drupal\\Component\\EventDispatcher\\ContainerAwareEventDispatcher-\u003Edispatch(\u0027kernel.request\u0027, Object(Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent))\n#9 \/var\/www\/html\/vendor\/symfony\/http-kernel\/HttpKernel.php(68): Symfony\\Component\\HttpKernel\\HttpKernel-\u003EhandleRaw(Object(Symfony\\Component\\HttpFoundation\\Request), 1)\n#10 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/StackMiddleware\/Session.php(57): Symfony\\Component\\HttpKernel\\HttpKernel-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#11 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/StackMiddleware\/KernelPreHandle.php(47): Drupal\\Core\\StackMiddleware\\Session-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#12 \/var\/www\/html\/web\/core\/modules\/page_cache\/src\/StackMiddleware\/PageCache.php(99): Drupal\\Core\\StackMiddleware\\KernelPreHandle-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#13 \/var\/www\/html\/web\/core\/modules\/page_cache\/src\/StackMiddleware\/PageCache.php(78): Drupal\\page_cache\\StackMiddleware\\PageCache-\u003Epass(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#14 \/var\/www\/html\/web\/modules\/contrib\/jsonapi\/src\/StackMiddleware\/FormatSetter.php(45): Drupal\\page_cache\\StackMiddleware\\PageCache-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#15 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/StackMiddleware\/ReverseProxyMiddleware.php(47): Drupal\\jsonapi\\StackMiddleware\\FormatSetter-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#16 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/StackMiddleware\/NegotiationMiddleware.php(52): Drupal\\Core\\StackMiddleware\\ReverseProxyMiddleware-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#17 \/var\/www\/html\/vendor\/stack\/builder\/src\/Stack\/StackedHttpKernel.php(23): Drupal\\Core\\StackMiddleware\\NegotiationMiddleware-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#18 \/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/DrupalKernel.php(693): Stack\\StackedHttpKernel-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#19 \/var\/www\/html\/web\/index.php(19): Drupal\\Core\\DrupalKernel-\u003Ehandle(Object(Symfony\\Component\\HttpFoundation\\Request))\n#20 {main}",
"trace":[
{
"file":"\/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/ParamConverter\/ParamConverterManager.php",
"line":100,
"function":"convert",
"class":"Drupal\\jsonapi\\ParamConverter\\EntityUuidConverter",
"type":"-\u003E",
"args":[
"d38dc101-4c91-4522-a0c6-56e86a660802",
{
"type":"entity:scrm_issue",
"converter":"paramconverter.jsonapi.entity_uuid"
},
"entity",
{
"_controller":"jsonapi.entity_resource:patchIndividual",
"resource_type":"scrm_issue--scrm_issue",
"_is_jsonapi":true,
"entity":"d38dc101-4c91-4522-a0c6-56e86a660802",
"_route":"jsonapi.scrm_issue--scrm_issue.individual.patch",
"_route_object":{
},
"_raw_variables":{
}
}
]
},
{
"file":"\/var\/www\/html\/web\/core\/lib\/Drupal\/Core\/Routing\/Enhancer\/ParamConversionEnhancer.php",
"line":45,
"function":"convert",
"class":"Drupal\\Core\\ParamConverter\\ParamConverterManager",
"type":"-\u003E",
"args":[
{
"_controller":"jsonapi.entity_resource:patchIndividual",
"resource_type":"scrm_issue--scrm_issue",
"_is_jsonapi":true,
"entity":"d38dc101-4c91-4522-a0c6-56e86a660802",
"_route":"jsonapi.scrm_issue--scrm_issue.individual.patch",
"_route_object":{
},
"_raw_variables":{
}
}
]
},
…
Sorry for this big response code, but I want to provide full information.
All entities created via JSON:API or Drupal UI get und
(?) langcode in JSON:API, but in fact, the default langcode for site is ru
.
And I don't understand how to patch it correctly for now. Trying to add prefix /und/ and /ru/ to the URL of JSON:API, and they throw 404 error.
Comment | File | Size | Author |
---|---|---|---|
#57 | 3043168-57.patch | 8.12 KB | Wim Leers |
#57 | interdiff.txt | 914 bytes | Wim Leers |
#48 | 3043168-48.patch | 8.12 KB | Wim Leers |
#48 | interdiff.txt | 4.01 KB | Wim Leers |
#43 | 3043168-43.patch | 7.5 KB | Wim Leers |
Comments
Comment #2
Wim LeersThanks for creating a separate issue for this! This makes it much easier to keep an overview :)
You linked to #2794431: [META] Formalize translations support, but I think you meant to link to #3038308: Avoid translations DELETE data loss and unintended changes with PATCH and test all methods against entity route parameter translation upcasting, which is where changes were recently introduced.
Comment #3
Wim LeersYou're running into the edge case that
\Drupal\Tests\jsonapi\Functional\JsonApiFunctionalMultilingualTest::testPatchTranslationFallback()
is explicitly testing.Trimmed the response body to just the relevant bits.
I think this is the root cause of all your problems. It sounds like your Content Translation settings are wrong. Pinging @plach to hopefully confirm :)
Comment #4
Max_Z. CreditAttribution: Max_Z. as a volunteer commentedHaving the same problem since 2.4.
My colleague has just found temporary fix - the patch request succeeds after adding to the node a translation to the main site language (the node has different language than main).
Comment #5
NiklanBut the problem is, my custom content entity type is not translatable. So there are no any content translation settings for it. 🤷 This entity type isn't showing on admin page (/admin/config/regional/content-language).
Seems like this is logical behavior and langcode for those entities remains
und
.Comment #6
plach@Niklan:
Is the site multilingual? Can you try PATCHing at /ru/node/foo?
Comment #7
Max_Z. CreditAttribution: Max_Z. as a volunteer commented@plach:
Just tested it.
When adding /fr/ - main site language, it throws the same error as before.
When adding /en/ - node language - 404 No route found
Comment #8
NiklanNo, it single language, but not English.
/ru/node/foo - not working, "No route found for \"PATCH /ru/jsonapi/node/page/1f276286-689e-4b3b-b160-aacd2ab203c8\"", but /jsonapi/node/page/1f276286-689e-4b3b-b160-aacd2ab203c8 is working as expected.
When I
GET
node via JSON:API, it contain"langcode": "ru",
in attributes. But my custom entity has no this value, since it hasn't langcode base field defintion.Comment #9
NiklanThe code inside
Drupal\jsonapi\ParamConverter\EntityUuidConverter
execute this condition, before exception.if ($method === 'PATCH' && $entity->language()->getId() !== $current_content_language) {
Above we have
if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
Every content entity is an instance of
TranslatableIntreface
sinceContentEntityBase
implements it, but not every content entity is translatable.I think, we should additionally check is entity is actually translatable using
TranslatableInterface::isTranslatable()
, as I understand it.Comment #10
NiklanUpload the patch, which currently fixes the problem. But I don't test it heavily at this moment.
UPD. Tested all my use cases for PATCHing untranslatable content entity, the patch is work.
Comment #11
plach@Niklan:
The weird thing is that if the site is monolingual, the current content language should always be
ru
, I think that's the condition to investigate/fix.@Max_Z.:
/en/...
won't work because by default the default language has no URL prefix configured. The intended behavior is that the negotiated content language must match the specified entity language, so ideally you would update English nodes on/
and French nodes on/fr/
.@Wim Leers:
We may need some additional tests around the use cases described here to validate that the logic as behaving is expected.
Comment #12
plach@Niklan:
Ah, now I get it! Your nodes are being created with
und
language, which of course doesn't match the current content language. To match the current logic you'd have to change all the node languages fromund
toru
and restore the default content language settings, so that nodes are always created in the default site language. Alternatively we could relax the current constraints and take non-translated entities into account, as you are suggesting. I'm not sure if that's desirable from a BC/FC perspective given that proper multilingual support is still in the works, this is a @Wim Leers choice :)Comment #13
plachComment #15
NiklanNo, they created with "ru" langcode. This is correct, and I have no problems with the node entity type. I don't even use them, only tested as you asked me.
I have a problem with custom content entity type, which is not translatable. This entities always get langcode set to
und
since they're not translatable at all, but they pass this conditionif ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
because
ContentEntityBase
implementsTranslatableInterface
. This means, every content entity type for Drupal is implementingTranslatableInterface
and this condition always returnsTRUE
for them. But! This not means that the given content entity type is actually translatable, because this is handled through annotationDrupal\Core\Entity\EntityType
protected $translatable = FALSE;
.For me, this means that all content entity types is not translatable by default, but implements
TranslatableInterface
. Those who need entity to be translated, must clearly set it in@ContentEntityType
annotation.For example, this is how it's done for node entity type.
But my content entity has no this value, so its
FALSE
by default.And that's why (I think so) this condition
if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
failed for my entity, but not for the node entity type.
This condition assumes by default, that given entity is translatable by default, since it implements
TranslatableInterface
, but this is not true. The real value is the current entity is translatable or not, returns byTranslatableInterface::isTranslatable()
.For example
$node->isTranslatable()
returnTRUE
, but my entity$my_entity_obj->isTranslatable()
returnFALSE
. And this is not check in any condition JSON:API. As I think, there is no need to compare language of entity and language of the request if entity is not support translations at all. It always will beund
for any case.Hopes now it's more clear that problem not in the language detection and content translation settings for this issue.
Comment #16
fbracq CreditAttribution: fbracq commentedI'm working with @Max_Z.
Website is multilingual with FR as default language. Not possible to PATCH nodes of custom content entity type created with language = EN. Tried to create FR translation for these contents, but now we are getting error like "Symfony\Component\HttpKernel\Exception\BadRequestHttpException: Updating a resource object that has a working copy is not yet supported. See https://www.drupal.org/project/jsonapi/issues/2795279. in Drupal\jsonapi\Controller\EntityResource->patchIndividual() (line 303 of /opt/bitnami/apps/drupal/htdocs/web/modules/contrib/jsonapi/src/Controller/EntityResource.php)."
Comment #17
joelstein CreditAttribution: joelstein at On Fire Media commentedI'm encountering this same 405 error when trying to PATCH or DELETE on a custom, non-translatable entity. The patch in #10 worked for me.
Comment #18
andypostI think there's misunderstood about issue
- jsonapi checks interface but should also respect "runtime" entity type settings, needs test
- routing (path prefix) - should respect language negotiation and node type translatability config
Comment #19
benjy CreditAttribution: benjy at Unearthed commentedSame error here with a custom entity that is not translatable, on a single language site, English.
The requested translation of the resource object does not exist, instead modify one of the translations that do exist: und.
Comment #20
Niklan@benjy did you try patch from #10? Did it solve the problems? Looks like this is easy to fix.
Comment #21
BramDriesenI'm having the same issue, but on a ML site where the custom entity is set to not translatable. The patch didn't fix the issue for me though.
Comment #22
BramDriesenDowngrading to 8.x-2.3 did solve it though, so it must be something introduced since 2.4.
Comment #23
TipiT CreditAttribution: TipiT commentedSame problem here. The content we create through JSONAPI is always English but when send the request to /de or /fi etc. language we get the error. And yes, I did work before the update 2.3->2.4.
Comment #24
BramDriesenLet's raise the priority since a lot of people seem to have this issue.
Comment #25
joelstein CreditAttribution: joelstein at On Fire Media commentedSince JSON API is now a core module in Drupal 8.7, I'm moving the issue accordingly. Also, here's an updated patch of #10 applied to Drupal core.
Comment #26
benjy CreditAttribution: benjy at Unearthed commented@Niklan, yes that patch worked for me.
Comment #27
gabesulliceA regression test in
JsonApiRegressionTest.php
that shows the problem would be extremely valuable. It's really difficult for me to tell if that all the users reporting problems here have the same problem as @Niklan.Thank you @Niklan for the original patch and thank you @joelstein for re-rolling it for core! I agree that this change is probably one we ought to make. I'm only concerned about how/if it will affect BC/FC as @plach pointed out in #12. Translations are an area of JSON:API (really, in general) where I have little expertise.
Comment #28
gabesulliceI tried to write a regression test that replicates the scenario @Niklan described in #15. Locally, the test passes fine for me, so I must be missing some nuance (or maybe it will fail on d.o).
@Niklan, can you look at this test and tell me if it accurately reflects the preconditions and operations that you have? And, if not, either update the test or tell me what is different?
Update: it looks like the test only patch passed, so there's definitely some part of this bug that I don't yet understand.
Comment #29
gabesulliceComment #30
Niklan@gabesullice I didn't know much about testing but read patch line-by-line with comments and it looks ok for me.
The most important part of it, which cause problem in my case
$this->assertFalse($entity->isTranslatable());
Looks like the test passed and there is no problem detected. I think I'll try to reproduce this bug on clean Drupal installation, since JSON:API in core now.
Comment #31
Wim Leers#3038308: Avoid translations DELETE data loss and unintended changes with PATCH and test all methods against entity route parameter translation upcasting specifically introduced test coverage to prove this is working correctly. We worked with translation system expert @plach to ensure all edge cases had test coverage.
But perhaps this is an edge case we didn't think about. Because the title says
, and based on the error response in the issue summary you're not using a language prefix, the content entity has langcodeund
, yet you're getting"The requested translation of the resource object does not exist, instead modify one of the translations that do exist: und."
, meaning this was triggered:That must mean that
$entity->language()->getId()
==='und'
, but$current_content_language
is something different, probably'ru'
.#28:
This is where the test is deviating from the reported problem AFAICT: this should be
und
. I'm not saying that would be correct, I'm saying that that would match the reported behavior.I'm not sure what the expected behavior here is. This is pretty deep entity multilingual stuff. Assigning to @Gábor Hojtsy for initial feedback, perhaps we should ask @plach later.
Comment #32
plach@Wim Leers:
Definitely, we were concerned about preventing data integrity issues with content translations and we overlooked non-translatable entities.
As mentioned in #12, I think the patch proposed in #10 (and then in #25) makes sense as a fix. My main doubt was about whether you were interested in making this work since you are planning to rework it completely anyway, given that this is not a data-loss scenario.
It should definitely be possible to update tests to account for this scenario and only apply to translatable entities the restrictions we introduced earlier. The only actual behavior change should be that the case reported here starts working again, so we should not have BC issues.
Comment #33
Max_Z. CreditAttribution: Max_Z. as a volunteer commentedApplied Patch#25, but 405 error is still there when patching EN node in FR website.
Comment #34
penyaskitoHaving the same issue with a pretty vanilla Drupal Commerce installation trying to update the status of a commerce_order. #25 worked for me.
Comment #35
cspitzlayComment #36
mglamanI opened duplicate #3067405: Cannot patch entities that are not translated (und != en). Same as #34. This shouldn't be postponed but NW due to missing tests. It's easily producible.
Comment #37
mglamanSorry, selected wrong status.
Comment #38
Wim LeersRebased #28, and incorporated my feedback from #31. Now it successfully reproduces the reported bug.
I tried to figure out how the issue reporter and @mglaman even get an entity with
langcode=und
and ended up debugging\Drupal\language\DefaultLanguageItem::applyDefaultValue()
but not finding it. But how we get to that point is actually irrelevant, so this test will do.Comment #39
Wim LeersAnd the fix is indeed what @Niklan suggested in #9.
Note that the same bug was introduced in
\Drupal\Core\ParamConverter\EntityRevisionParamConverter::convert()
by #2808163: Support dynamic entity types in the EntityRevisionParamConverter…Comment #42
Wim LeersThe good news is that #38 successfully reproduced the reported bug, and #39 fixed it :)
But it looks like
JsonApiFunctionalMultilingualTest
is missing some configuration that is causingNode::isTranslatable()
to incorrectly returnFALSE
in a few cases. And that's causing failures in the test now.Too bad, otherwise I'd have RTBC'd this 🤓
Comment #43
Wim LeersThat was indeed the cause. Simple fix.
\Drupal\Core\Entity\ContentEntityBase::isTranslatable()
checks a key that is only ever set bycontent_translation_entity_bundle_info_alter()
(some hidden but understandable coupling there), which led me to\Drupal\content_translation\ContentTranslationManager::isEnabled()
, which pointed to\Drupal\content_translation\ContentTranslationManager::setEnabled()
…Assigning to @plach because he's definitely the core committer who should sign off on this :)
Comment #44
mglaman🙌Thank you @Wim Leers! I had one thought:
There is Drupal\Core\Entity\TranslatableInterface would satisfy both of the interface checks (the Translatable interface check is against the TypedData one.)
Not sure if that's something we'd change in this patch as the main thing is to add
isTranslatable
. But it'd reduce cognitive load on reading the logic statement and fix a possibly redundant check.Comment #45
Wim LeersI'd agree with you if it weren't for the fact that
\Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase::getEntitiesToView()
and others in core also use that sameinstanceof
check :)Comment #46
catchTagging to back up assigning.
Comment #47
plachThe patch looks very good, thanks! Only very minor tweaks needed:
I agree with @mglaman that it would be better to use
Drupal\Core\Entity\TranslatableInterface
: this was introduced more recently than the typed data one and we should definitely start adopting it whenever possible.I don't think we do this normally.
Let's use
LanguageInterface::LANGCODE_NOT_SPECIFIED
instead of'und'
.@Wim Leers, #43:
Well, actually the reason why we choose this approach was exactly to avoid coupling a core system with a module. In fact it should definitely be possible to test this by altering the bundle info via a test module alter hook + a state flag. However, since we are already using CT in the test, it's probably worth relying on it also for this :)
Comment #48
Wim LeersBigPipeRegressionTest()
.Comment #49
plachSaving credits
Comment #52
plachCommitted and pushed to 8.7.x and 8.8.x, thanks!
Comment #54
larowlanRolled this back on 8.8.x, it adds a deprecation error
Comment #55
larowlanI expect that will also fail on 8.7.x, watching the HEAD testing and will revert if so
Comment #56
larowlan8.7.x is good 🎉
Comment #57
Wim LeersD'oh!
Comment #58
larowlanCommitted 5699dcb and pushed to 8.8.x. Thanks!