Problem/Motivation
#3613823: Replace Accept-Language read negotiation with URL-keyed language selection (langCode + includeFallback) made reads URL-keyed: the langCode query parameter (optionally with includeFallback=1) is the only read-side language selector, and language request headers are inert on reads. Writes, however, still accept two selectors: the same langCode query parameter and the Content-Language request header, documented as "functionally identical and interchangeable".
The second selector earns nothing and costs real surface:
- a mismatch-validation branch in
getResourceLanguage()for the case where both selectors are present and disagree (its own 422, its own test matrix); - two
Content-Languagebranches inEntityAccessChecker::getEntityTranslation()(content and config), reachable only through writes with?include=; - a header parameter on every write operation in the generated OpenAPI document;
- the "headers are inert on cacheable requests" guard commentary that exists only to fence the header off from the read path;
- a residual header dependency in a contract whose whole point, since #3613823: Replace Accept-Language read negotiation with URL-keyed language selection (langCode + includeFallback), is that language state lives entirely in the URL.
Every documented client path already works with langCode alone, and the planned api_client adapter sends langCode on writes and never sets Content-Language.
Steps to reproduce
Not a bug; current behavior is as designed. PATCH /jsonapi/node/article/{uuid} with Content-Language: fr and no langCode currently patches the fr translation; after this change it is rejected (see below).
Proposed resolution
Retire the Content-Language request header as a language selector. langCode becomes the single selector for every operation, reads and writes alike.
- A write carrying the retired header is rejected with
400 Bad Request("The "Content-Language" request header is not supported. Use the "langCode" query parameter to select the language."), not silently ignored. Silence is the dangerous option: a legacy client'sPATCHwith only the header would land on the default translation and overwrite it with the other language's values, and a header-onlyDELETEwould have no language target at all and delete the whole entity. The rejection fires even when the header agrees with a validlangCode, so the retirement is unambiguous. Writes are never cacheable, so the header-dependent 400 has none of the cache-poisoning exposure that drove the read-side inertness rule (#3613686). - Reads are unchanged: language request headers stay inert,
Content-Languageexactly likeAccept-Language, for the URL-keyed cache-safety reasons established in #3613823. - The
Content-Languageresponse header is untouched. Responses keep labeling themselves with the language actually served. - Deleted: the header read and the langCode-vs-header mismatch 422 in
getResourceLanguage(); both header branches inEntityAccessChecker::getEntityTranslation(); the write-side header parameter in the OpenAPI generator. The payload-vs-metadata rule is unchanged: a payloadlangcodeattribute, if present, must still match the targeted language. - Companion simplification, enabled by the same single-selector contract: the
jsonapi_menu_itemsrequest attribute becomes a scalar (ATTR_MENU_ITEMS_LANGCODE) instead of the Accept-Language-era "ordered list of one", removing the array guards and candidate loop fromRequestLanguageEntityRepository,MenuItemsLanguagePathProcessorand the response-labeling listener; andgetCollectionQuery()stops re-declaring the language cache contexts thataddLanguageCacheability()already declares on every translatable collection response. - Divergence tracking: core #3199697 accepts
Content-Languageon writes, so this is the module's third trackedDIVERGENCE(core):departure (after the ignoredAccept-Languageread header and the additiveincludeFallbackparameter). The marker carries the usual propose-upstream-or-restore plan; the upstream argument is the same single-selector one: one selector to validate, the whole contract in the URL. - Specs (
translation-endpoints,core-alignment,menu-translation,openapi-integration), README, CONTRIBUTING examples and hook_help are updated to the single-selector contract.
API changes
Breaking for any client that selects a write's language with the Content-Language request header: such writes now return 400 Bad Request with migration guidance in the error detail. The two selectors were documented as interchangeable, so the migration is mechanical: send ?langCode=<langcode> instead. Clients already using langCode, and all reads, are unchanged. The generated OpenAPI document no longer lists a Content-Language parameter on write operations. The Content-Language response header is unchanged.
Issue fork jsonapi_multilingual-3614200
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
lauriii