Problem/Motivation
I'm working with Core's REST module, and I've extended a custom entity's resource. In the delete method, I want to return some extra data about the deletion, but I always run into the following exception:
Symfony\Component\Serializer\Exception\NotEncodableValueException: Serialization for the format is not supported in Symfony\Component\Serializer\Serializer->serialize() (line 112 of /var/www/html/vendor/symfony/serializer/Serializer.php).
Drupal\rest\EventSubscriber\ResourceResponseSubscriber->Drupal\rest\EventSubscriber\{closure}() (Line: 582)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 177)
Drupal\rest\EventSubscriber\ResourceResponseSubscriber->renderResponseBody(Object, Object, Object, NULL) (Line: 76)
Drupal\rest\EventSubscriber\ResourceResponseSubscriber->onResponse(Object, 'kernel.response', Object)
call_user_func(Array, Object, 'kernel.response', Object) (Line: 111)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.response', Object) (Line: 191)
Symfony\Component\HttpKernel\HttpKernel->filterResponse(Object, Object, 1) (Line: 173)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 49)
Asm89\Stack\Cors->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 693)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)POST, GET, PATCH all work fine. And all my code inside the resource also works fine. But during the serialization it freaks out. I vaguely remember rest doing some wonky stuff to determine what format to use, and that was a problem for I believe file uploads? Where what was sent doesn't match what should be sent back. I'm thinking this may be the same here, as DELETEs don't receive a body for entities.
I send back a ModifiedResourceResponse with code 202 and some data. I've tried sending back the entity (as I don't delete the entity itself at that point in time), an array of some basic data, a string. The only thing that works is NULL. I've also tried 200, 201, all of 'em basically. No difference.
Steps to reproduce
1. Enable the core rest module
2. Create custom resource plugin with a DELETE method, this function will return a ResourceResponse with content:
/**
* Provides an example resource.
*
* @RestResource(
* id = "example_resource",
* label = @Translation("Example resource"),
* uri_paths = {
* "canonical" = "/api/example/{id}",
* }
* )
*/
class ExampleResource extends ResourceBase {
/**
* Delete an example entity based on ID.
*
* @param string $id
* The example ID.
*
* @return \Drupal\rest\ResourceResponse
* The resource response.
*/
public function delete(string $id): ResourceResponse {
return new ResourceResponse(['test123'], 200);
}
}3. Configure & enable the plugin:
status: true
dependencies:
module:
- example_module
- serialization
- user
id: example_resource
plugin_id: example_resource
granularity: resource
configuration:
methods:
- DELETE
formats:
- json
authentication:
- cookie4. Send delete request to /api/example/123
5. Returns statuscode 500; log: TypeError: Symfony\Component\Serializer\Encoder\ChainEncoder::getEncoder(): Argument #1 ($format) must be of type string, null given, called in /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php on line 49 in Symfony\Component\Serializer\Encoder\ChainEncoder->getEncoder() (line 80 of /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php)
Proposed resolution
Allow custom rest plugin DELETE endpoints to return a response body. According to https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.5-4, it should be allowed to return a 200 status code with extra information in the response. It's also possible to use statuscode 204, and omit the response body.
Remaining tasks
Test coverage
API changes
Delete routes that are dynamically generated will have the additional _format requirement applied to them.
| Comment | File | Size | Author |
|---|---|---|---|
| #14 | 3093973-14-will-fail.patch | 1.54 KB | robindh |
Issue fork drupal-3093973
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
JonasSavs commentedHi there,
Normally this could be achieved with:
Best regards.
Comment #9
robindh commentedSo I just ran into the same issue, when I wanted to return a validation message after failing to delete an entity.
I'll summarize some debugging in case anyone else stumbles onto this issue:
ResourceResponseSubscriber::getResponseFormat()will determine the format to return the response in.This will check the route requirements that are dynamically generated by the rest module; e.g. _format & _content_type_format.
For delete routes, format NULL is returned, since dynamically generated DELETE routes do not contain _format or _content_type_format requirements. This is intentional, according to documentation in
ResourceResponseSubscriber::renderResponseBody().The symfony serializer does not seem to support this (anymore?). Also, the $format parameter is string typed now, so passing a NULL value as format now throws
Symfony\Component\Serializer\Encoder\ChainEncoder::getEncoder(): Argument #1 ($format) must be of type string, null givenin there. Not just a NotEncodableValueException, like in the original issue report.For now, I have fixed the issue on my project by using a RouteSubscriber that alters the dynamically generated route by the rest module. Example snippet:
This will make sure a format ('json' in this case) is passed to the symfony serializer; and avoid the fatal error from being thrown.
The easier solution seems to be expanding the array of request methods in
ResourceRoutes::getRoutesForResourceConfig()to include the delete method, not sure yet if this will break some functionality.Comment #11
robindh commentedIn https://www.drupal.org/project/drupal/issues/3277148, documentation was added to indicate _format requirement must always be present when returning a ResourceResponse.
Created a merge request that will add the _format requirement to DELETE routes, to check which tests will break
Comment #12
robindh commentedSteps to reproduce on a clean drupal install:
TypeError: Symfony\Component\Serializer\Encoder\ChainEncoder::getEncoder(): Argument #1 ($format) must be of type string, null given, called in /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php on line 49 in Symfony\Component\Serializer\Encoder\ChainEncoder->getEncoder() (line 80 of /var/www/html/vendor/symfony/serializer/Encoder/ChainEncoder.php)I don't really understand the assumption where DELETE requests cannot contain a response body; according to https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.5-4, it should be allowed to return a 200 status code with extra information in the response. It's also possible to use statuscode 204, and omit the response body.
Any maintainers willing to weigh in on this?
Comment #13
smustgrave commentedIssue summary should be updated with proposed solution, any api changes, etc.
Also will need a test case showing the issue.
Thanks!
Comment #14
robindh commentedI've reformatted the issue summary and attached a patch containing a test that should fail.
It mocks a DELETE endpoint rest plugin (no _format route requirement defined), where some content is returned.
Comment #15
robindh commentedComment #16
smustgrave commentedRemoving the needs tests as #14 shows a valid failure.
But can the test be added to the MR to show that the change passes the valid test you uploaded
Thanks!
Comment #18
robindh commentedThe test was actually already included in the merge request, since I've removed the
$method !== 'DELETE'conditionals from existing tests (testOnResponseWithCacheableResponse&testOnResponseWithUncacheableResponse).This will make the existing assertions for the other request methods trigger for DELETE method requests too.
Comment #19
smustgrave commentedReran the tests on the MR just to confirm it was random failure before (it was).
Comment #20
catchComment #24
catchCommitted/pushed to 10.1.x and cherry-picked to 10.0.x and 9.5.x, thanks!